From 53355a0e2eb601ff6627b5825c8748c169370b39 Mon Sep 17 00:00:00 2001 From: Dimitar Yanakiev <31411471+dkyanakiev@users.noreply.github.com> Date: Thu, 30 Nov 2023 22:40:37 +0200 Subject: [PATCH] Bug fixing and improvements for 0.0.3 (#4) * Changes * Headscratcher * v0.0.3 fixes --- CHANGELOG.md | 15 +++++++ Makefile | 1 + cmd/vaul7y/main.go | 71 +++++++++++++++++------------- component/commands.go | 15 ++++--- component/info.go | 4 -- component/logo.go | 14 +++++- component/logo_test.go | 7 ++- component/mounts_table.go | 2 +- component/policy_acl_table.go | 7 ++- component/search.go | 2 +- component/secrets_table.go | 1 - component/selector.go | 82 +++++++++++++++++++++++++++++++++++ config/logger.go | 15 ++++--- go.mod | 1 + go.sum | 3 ++ helpers/setup.sh | 14 +++--- primitives/selector.go | 38 ++++++++++++++++ state/state.go | 3 ++ vault/client.go | 9 ++++ vault/vaultfakes/fake_sys.go | 70 ++++++++++++++++++++++++++++++ view/history.go | 6 +++ view/init.go | 5 ++- view/inputs.go | 7 +-- view/mounts.go | 18 ++++++-- view/policy.go | 8 +++- view/secretobj.go | 40 ++++++++++++++--- view/secrets.go | 77 +++++++++++++++++++++++++++++++- view/view.go | 33 +++++++------- 28 files changed, 467 insertions(+), 101 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 component/selector.go create mode 100644 primitives/selector.go diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0ae7389 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +## [0.0.3] - 2023-11-30 + +### Added +- Job filtering on secrets and mount views +- Better navigation options between views +- `vaul7y -v` to check the version +- Added a check and error out to prevent vaul7y from freezing if vault token and address are not set + +### Fixed +- Error and Info modals tabbing out and changing focus +- Enter key constantly moving you to the Secret Engines view. Its due to the way Unix system recognize Enter and Ctrl+M +- Fixed an issue with watcher causing conflicts +- Fixed logger to discard messages and not brake rendering when debugging is not enabled diff --git a/Makefile b/Makefile index 634eb57..b1b3d2a 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,4 @@ + local-vault: vault server -dev diff --git a/cmd/vaul7y/main.go b/cmd/vaul7y/main.go index 92207a4..dfa2f7c 100644 --- a/cmd/vaul7y/main.go +++ b/cmd/vaul7y/main.go @@ -1,7 +1,9 @@ package main import ( + "fmt" "log" + "os" "time" "github.com/dkyanakiev/vaulty/component" @@ -11,13 +13,33 @@ import ( "github.com/dkyanakiev/vaulty/view" "github.com/dkyanakiev/vaulty/watcher" "github.com/gdamore/tcell/v2" + "github.com/jessevdk/go-flags" "github.com/rivo/tview" ) -var refreshIntervalDefault = time.Second * 5 +var refreshIntervalDefault = time.Second * 30 +var version = "0.0.3" + +type options struct { + Version bool `short:"v" long:"version" description:"Show Damon version"` +} func main() { + // Check for required Vault env vars + checkForVaultAddress() + + var opts options + _, err := flags.ParseArgs(&opts, os.Args) + if err != nil { + os.Exit(1) + } + + if opts.Version { + fmt.Println("vaul7y", version) + os.Exit(0) + } + logFile, logger := config.SetupLogger() defer logFile.Close() tview.Styles.PrimitiveBackgroundColor = tcell.NewRGBColor(40, 44, 48) @@ -25,6 +47,7 @@ func main() { vaultClient, err := vault.New(func(v *vault.Vault) error { return vault.Default(v, logger) }) + state := initializeState(vaultClient) commands := component.NewCommands() vaultInfo := component.NewVaultInfo() @@ -33,7 +56,7 @@ func main() { policyAcl := component.NewPolicyAclTable() secrets := component.NewSecretsTable() secretObj := component.NewSecretObjTable() - logo := component.NewLogo() + logo := component.NewLogo(version) info := component.NewInfo() failure := component.NewInfo() errorComp := component.NewError() @@ -53,8 +76,9 @@ func main() { } watcher := watcher.NewWatcher(state, vaultClient, refreshIntervalDefault, logger) view := view.New(components, watcher, vaultClient, state, logger) + view.Init(version) - view.Init("0.0.1") + //view.Init("0.0.1") err = view.Layout.Container.Run() if err != nil { log.Fatal("cannot initialize view.") @@ -65,36 +89,23 @@ func main() { func initializeState(client *vault.Vault) *state.State { state := state.New() addr := client.Address() + version, _ := client.Version() state.VaultAddress = addr + state.VaultVersion = version state.Namespace = "default" return state } -// // LOOK AT LATER -// func main() { -// vaultClient, _ := vault.New(vault.Default) -// //ctx := context.TODO() -// // mounts, _ := vaultClient.Sys.ListMounts() - -// secret, _ := vaultClient.ListSecrets("kv0FF76557") -// log.Println(secret) - -// secrets, _ := vaultClient.ListNestedSecrets("kv0FF76557", "") -// //secrets, err := vaultClient.Logical.List("randomkv/metadata/test/one") - -// for _, value := range secrets { -// fmt.Printf("Key: %s\n", value.PathName) -// fmt.Printf("IsSecret: %t\n", value.IsSecret) -// } -// // val, err := vaultClient.KV2.Get(ctx, "path") -// // fmt.Println(val) -// // fmt.Println(err) - -// // secretClient, err := vaultClient.Logical.List("credentials/metadata/") -// // if err != nil { -// // // TODO -// // fmt.Println(err) -// // } -// // vault.DataIterator(secretClient.Data["keys"]) -// } +func checkForVaultAddress() { + if os.Getenv("VAULT_ADDR") == "" { + fmt.Println("VAULT_ADDR is not set. Please set it and try again.") + os.Exit(1) + } + + if os.Getenv("VAULT_TOKEN") == "" { + fmt.Println("VAULT_TOKEN is not set. Please set it and try again.") + os.Exit(1) + } + +} diff --git a/component/commands.go b/component/commands.go index 5c67f1e..9411396 100644 --- a/component/commands.go +++ b/component/commands.go @@ -13,35 +13,36 @@ import ( var ( MainCommands = []string{ fmt.Sprintf("%sMain Commands:", styles.HighlightSecondaryTag), - fmt.Sprintf("%s%s to display System Mounts", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("%s%s to display Secret Engines", styles.HighlightPrimaryTag, styles.StandardColorTag), fmt.Sprintf("%s%s to display ACL Policies", styles.HighlightPrimaryTag, styles.StandardColorTag), fmt.Sprintf("%s%s to Quit", styles.HighlightPrimaryTag, styles.StandardColorTag), } MountsCommands = []string{ - fmt.Sprintf("\n%s Secret Mounts Command List:", styles.HighlightSecondaryTag), - fmt.Sprintf("%s%s to explore mount", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("\n%s Secret Engines Command List:", styles.HighlightSecondaryTag), + fmt.Sprintf("%s or %s to explore mount", styles.HighlightPrimaryTag, styles.StandardColorTag), } NoViewCommands = []string{} PolicyCommands = []string{ fmt.Sprintf("\n%s ACL Policy Commands:", styles.HighlightSecondaryTag), - fmt.Sprintf("%s%s to inspect policy", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("%s or %s to inspect policy", styles.HighlightPrimaryTag, styles.StandardColorTag), fmt.Sprintf("%s%s apply filter", styles.HighlightPrimaryTag, styles.StandardColorTag), } PolicyACLCommands = []string{ fmt.Sprintf("\n%s ACL Policy Commands:", styles.HighlightSecondaryTag), - fmt.Sprintf("%s%s to go back", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("%s%s to go back", styles.HighlightPrimaryTag, styles.StandardColorTag), //fmt.Sprintf("%s%s apply filter", styles.HighlightPrimaryTag, styles.StandardColorTag), } SecretsCommands = []string{ fmt.Sprintf("\n%s Secrets Commands:", styles.HighlightSecondaryTag), - fmt.Sprintf("%s%s to navigate to selected the path", styles.HighlightPrimaryTag, styles.StandardColorTag), - fmt.Sprintf("%s%s to go back to the previous path", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("%s or %s to navigate to selected the path", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("%s or %s to go back to the previous path", styles.HighlightPrimaryTag, styles.StandardColorTag), } SecretObjectCommands = []string{ fmt.Sprintf("\n%s Secret Commands:", styles.HighlightSecondaryTag), fmt.Sprintf("%s%s toggle display for secrets", styles.HighlightPrimaryTag, styles.StandardColorTag), fmt.Sprintf("%s%s copy secret to clipboard", styles.HighlightPrimaryTag, styles.StandardColorTag), fmt.Sprintf("%s%s toggle json view for secret", styles.HighlightPrimaryTag, styles.StandardColorTag), + fmt.Sprintf("%s or %s to go back to the previous path", styles.HighlightPrimaryTag, styles.StandardColorTag), //TODO: Work in progress //fmt.Sprintf("%s

%s patch secret", styles.HighlightPrimaryTag, styles.StandardColorTag), } diff --git a/component/info.go b/component/info.go index 5a35f27..f3038c0 100644 --- a/component/info.go +++ b/component/info.go @@ -38,10 +38,6 @@ func (i *Info) Render(msg string) error { return ErrComponentNotBound } - i.Props.Done = func(buttonIndex int, buttonLabel string) { - i.pages.RemovePage(PageNameInfo) - - } i.Modal.SetDoneFunc(i.Props.Done) i.Modal.SetText(msg) i.pages.AddPage(PageNameInfo, i.Modal.Container(), true, true) diff --git a/component/logo.go b/component/logo.go index 797f88d..5aa8ee9 100644 --- a/component/logo.go +++ b/component/logo.go @@ -1,6 +1,7 @@ package component import ( + "fmt" "strings" primitive "github.com/dkyanakiev/vaulty/primitives" @@ -21,12 +22,20 @@ var LogoASCII = []string{ type Logo struct { TextView TextView slot *tview.Flex + Props *LogoProps } -func NewLogo() *Logo { +type LogoProps struct { + Version string +} + +func NewLogo(version string) *Logo { t := primitive.NewTextView(tview.AlignRight) return &Logo{ TextView: t, + Props: &LogoProps{ + Version: version, + }, } } @@ -35,8 +44,9 @@ func (l *Logo) Render() error { return ErrComponentNotBound } + versionText := fmt.Sprintf("[#26ffe6]version: %s", l.Props.Version) logo := strings.Join(LogoASCII, "\n") - + logo = fmt.Sprintf("%s\n%s", logo, versionText) l.TextView.SetText(logo) l.slot.AddItem(l.TextView.Primitive(), 0, 1, false) return nil diff --git a/component/logo_test.go b/component/logo_test.go index 16af84f..607cc7f 100644 --- a/component/logo_test.go +++ b/component/logo_test.go @@ -2,6 +2,7 @@ package component_test import ( "errors" + "fmt" "strings" "testing" @@ -15,7 +16,7 @@ func TestLogo_Pass(t *testing.T) { r := require.New(t) textView := &componentfakes.FakeTextView{} - logo := component.NewLogo() + logo := component.NewLogo("0.0.0") logo.TextView = textView logo.Bind(tview.NewFlex()) @@ -24,13 +25,15 @@ func TestLogo_Pass(t *testing.T) { r.NoError(err) text := textView.SetTextArgsForCall(0) + versionText := fmt.Sprintf("[#26ffe6]version: %s", "0.0.0") expectedLogo := strings.Join(component.LogoASCII, "\n") + expectedLogo = fmt.Sprintf("%s\n%s", expectedLogo, versionText) r.Equal(text, expectedLogo) } func TestLogo_Fail(t *testing.T) { r := require.New(t) - logo := component.NewLogo() + logo := component.NewLogo("0.0.0") err := logo.Render() r.Error(err) diff --git a/component/mounts_table.go b/component/mounts_table.go index 8200545..17daea2 100644 --- a/component/mounts_table.go +++ b/component/mounts_table.go @@ -14,7 +14,7 @@ import ( ) const ( - TableTitleMounts = "System Mounts" + TableTitleMounts = "Secret Engines" ) var ( diff --git a/component/policy_acl_table.go b/component/policy_acl_table.go index 8fbf0af..424a79e 100644 --- a/component/policy_acl_table.go +++ b/component/policy_acl_table.go @@ -17,10 +17,9 @@ var ( type SelectPolicyACLFunc func(policyName string) type PolicyAclTable struct { - TextView TextView - InputField InputField - Props *PolicyAclTableProps - Flex *tview.Flex + TextView TextView + Props *PolicyAclTableProps + Flex *tview.Flex //Not sure I will use this Renderer *glamour.TermRenderer diff --git a/component/search.go b/component/search.go index 55af34a..b60991c 100644 --- a/component/search.go +++ b/component/search.go @@ -7,7 +7,7 @@ import ( "github.com/rivo/tview" ) -const searchPlaceholder = "(hit enter or esc to leave)" +const searchPlaceholder = "(hit esc to leave the filter)" type SearchField struct { InputField InputField diff --git a/component/secrets_table.go b/component/secrets_table.go index 1d53288..a2838cf 100644 --- a/component/secrets_table.go +++ b/component/secrets_table.go @@ -76,7 +76,6 @@ func (s *SecretsTable) GetIDForSelection() (string, string) { } func (s *SecretsTable) Render() error { - s.reset() fullPath := fmt.Sprintf("%s%s", s.Props.SelectedMount, s.Props.SelectedPath) s.Table.SetTitle("%s (%s)", TableTitleMounts, fullPath) diff --git a/component/selector.go b/component/selector.go new file mode 100644 index 0000000..028286f --- /dev/null +++ b/component/selector.go @@ -0,0 +1,82 @@ +package component + +import ( + "github.com/dkyanakiev/vaulty/primitives" + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +const pageNameSelector = "selector" + +type SelectorModal struct { + Modal Selector + Props *SelectorProps + pages *tview.Pages + keyBindings map[tcell.Key]func() +} + +type SelectorProps struct { + Items []string + AllocationID string +} + +func NewSelectorModal() *SelectorModal { + s := &SelectorModal{ + Modal: primitives.NewSelectionModal(), + Props: &SelectorProps{}, + keyBindings: map[tcell.Key]func(){}, + } + + s.Modal.GetTable().SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { + if fn, ok := s.keyBindings[event.Key()]; ok { + fn() + } + + return event + }) + + return s +} + +func (s *SelectorModal) Render() error { + if s.pages == nil { + return ErrComponentNotBound + } + + if s.Props.Items == nil { + return ErrComponentPropsNotSet + } + + table := s.Modal.GetTable() + table.Clear() + + for i, v := range s.Props.Items { + table.RenderRow([]string{v}, i, tcell.ColorWhite) + } + + s.Modal.GetTable().SetTitle("Select a Task (alloc: %s)", s.Props.AllocationID) + + s.pages.AddPage(pageNameSelector, s.Modal.Container(), true, true) + + return nil +} + +func (s *SelectorModal) Bind(pages *tview.Pages) { + s.pages = pages +} + +func (s *SelectorModal) SetSelectedFunc(fn func(task string)) { + s.Modal.GetTable().SetSelectedFunc(func(row, column int) { + task := s.Modal.GetTable().GetCellContent(row, 0) + fn(task) + s.Close() + }) +} + +func (s *SelectorModal) Close() { + s.pages.RemovePage(pageNameSelector) +} + +func (s *SelectorModal) BindKey(key tcell.Key, fn func()) { + s.keyBindings[key] = fn +} diff --git a/config/logger.go b/config/logger.go index 233a9c8..d09364f 100644 --- a/config/logger.go +++ b/config/logger.go @@ -8,12 +8,13 @@ import ( ) func SetupLogger() (*os.File, *zerolog.Logger) { - // UNIX Time is faster and smaller than most timestamps zerolog.TimeFieldFormat = zerolog.TimeFormatUnix logLevel, debugOn := os.LookupEnv("VAULTY_LOG_LEVEL") + var logger zerolog.Logger + // Default level for this example is info, unless debug flag is present if debugOn { level, err := zerolog.ParseLevel(logLevel) @@ -21,7 +22,12 @@ func SetupLogger() (*os.File, *zerolog.Logger) { log.Fatal().Err(err).Msg("Invalid log level") } zerolog.SetGlobalLevel(level) + logger = zerolog.New(os.Stdout).With().Timestamp().Logger() + } else { + // If debugOn is false, discard all log messages + logger = zerolog.Nop() } + var logFile *os.File // Check if file for logging is set @@ -31,11 +37,8 @@ func SetupLogger() (*os.File, *zerolog.Logger) { if err != nil { log.Panic().Err(err).Msg("Error opening log file") } - log.Logger = log.Output(zerolog.ConsoleWriter{Out: logFile, TimeFormat: zerolog.TimeFieldFormat}) - } else { - // If no log file is set, write to stdout - log.Logger = zerolog.New(os.Stdout).With().Timestamp().Logger() + logger = logger.Output(zerolog.ConsoleWriter{Out: logFile, TimeFormat: zerolog.TimeFieldFormat}) } - return logFile, &log.Logger + return logFile, &logger } diff --git a/go.mod b/go.mod index ae654f6..f79aeaf 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/charmbracelet/glamour v0.6.0 github.com/gdamore/tcell/v2 v2.6.0 github.com/hashicorp/vault/api v1.10.0 + github.com/jessevdk/go-flags v1.5.0 github.com/rivo/tview v0.0.0-20230907083354-a39fe28ba466 github.com/rs/zerolog v1.31.0 github.com/stretchr/testify v1.8.4 diff --git a/go.sum b/go.sum index b18c634..41a1045 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/vault/api v1.10.0 h1:/US7sIjWN6Imp4o/Rj1Ce2Nr5bki/AXi9vAW3p2tOJQ= github.com/hashicorp/vault/api v1.10.0/go.mod h1:jo5Y/ET+hNyz+JnKDt8XLAdKs+AM0G5W0Vp1IrFI8N8= +github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= +github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -155,6 +157,7 @@ golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/helpers/setup.sh b/helpers/setup.sh index 9c959b2..faab9eb 100755 --- a/helpers/setup.sh +++ b/helpers/setup.sh @@ -6,17 +6,17 @@ export VAULT_ADDR='http://127.0.0.1:8200' # Login to Vault vault login $VAULT_TOKEN -# Create multiple KV v2 stores with random names -for i in {1..5} -do - kv_store_name="kv$(uuidgen | cut -c1-8)" - vault secrets enable -version=2 -path=$kv_store_name kv -done +# # Create multiple KV v2 stores with random names +# for i in {1..10} +# do +# kv_store_name="kv$(uuidgen | cut -c1-8)" +# vault secrets enable -version=2 -path=$kv_store_name kv +# done # Create random secrets in each KV store for kv_store_name in $(vault secrets list -format=json | jq -r 'to_entries[] | select(.value.type == "kv") | .key') do - for j in {1..5} + for j in {1..100} do # Create a secret at the root of the KV store vault kv put $kv_store_name/data/secret$j key1=$(openssl rand -base64 12) key2=$(openssl rand -base64 12) diff --git a/primitives/selector.go b/primitives/selector.go new file mode 100644 index 0000000..bf1978a --- /dev/null +++ b/primitives/selector.go @@ -0,0 +1,38 @@ +package primitives + +import ( + "github.com/rivo/tview" +) + +type SelectionModal struct { + Table *Table + container *tview.Flex +} + +func NewSelectionModal() *SelectionModal { + t := NewTable() + f := tview.NewFlex(). + AddItem(nil, 0, 1, false). + AddItem(tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(nil, 0, 1, false). + AddItem(t.primitive, 10, 1, false). + AddItem(nil, 0, 1, false), 80, 1, false). + AddItem(nil, 0, 1, false) + + return &SelectionModal{ + Table: t, + container: f, + } +} + +func (s *SelectionModal) Container() tview.Primitive { + return s.container +} + +func (s *SelectionModal) Primitive() tview.Primitive { + return s.Table.primitive +} + +func (s *SelectionModal) GetTable() *Table { + return s.Table +} diff --git a/state/state.go b/state/state.go index 5ce06bc..12988cc 100644 --- a/state/state.go +++ b/state/state.go @@ -8,6 +8,7 @@ import ( type State struct { VaultAddress string + VaultVersion string Mounts map[string]*models.MountOutput SecretsData []models.SecretPath KV2 []models.KVSecret @@ -25,6 +26,7 @@ type State struct { Elements *Elements Toggle *Toggle Filter *Filter + Version string } type Toggle struct { @@ -33,6 +35,7 @@ type Toggle struct { } type Filter struct { + Object string Policy string } diff --git a/vault/client.go b/vault/client.go index 3af775a..a246313 100644 --- a/vault/client.go +++ b/vault/client.go @@ -36,6 +36,7 @@ type Sys interface { ListMounts() (map[string]*api.MountOutput, error) ListPolicies() ([]string, error) GetPolicy(name string) (string, error) + Health() (*api.HealthResponse, error) //ListMounts() ([]*api.Sys, error) } @@ -80,3 +81,11 @@ func Default(v *Vault, log *zerolog.Logger) error { func (v *Vault) Address() string { return v.Client.Address() } + +func (v *Vault) Version() (string, error) { + health, err := v.Sys.Health() + if err != nil { + return "", err + } + return health.Version, nil +} diff --git a/vault/vaultfakes/fake_sys.go b/vault/vaultfakes/fake_sys.go index 51a4200..8799456 100644 --- a/vault/vaultfakes/fake_sys.go +++ b/vault/vaultfakes/fake_sys.go @@ -22,6 +22,18 @@ type FakeSys struct { result1 string result2 error } + HealthStub func() (*api.HealthResponse, error) + healthMutex sync.RWMutex + healthArgsForCall []struct { + } + healthReturns struct { + result1 *api.HealthResponse + result2 error + } + healthReturnsOnCall map[int]struct { + result1 *api.HealthResponse + result2 error + } ListMountsStub func() (map[string]*api.MountOutput, error) listMountsMutex sync.RWMutex listMountsArgsForCall []struct { @@ -114,6 +126,62 @@ func (fake *FakeSys) GetPolicyReturnsOnCall(i int, result1 string, result2 error }{result1, result2} } +func (fake *FakeSys) Health() (*api.HealthResponse, error) { + fake.healthMutex.Lock() + ret, specificReturn := fake.healthReturnsOnCall[len(fake.healthArgsForCall)] + fake.healthArgsForCall = append(fake.healthArgsForCall, struct { + }{}) + stub := fake.HealthStub + fakeReturns := fake.healthReturns + fake.recordInvocation("Health", []interface{}{}) + fake.healthMutex.Unlock() + if stub != nil { + return stub() + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *FakeSys) HealthCallCount() int { + fake.healthMutex.RLock() + defer fake.healthMutex.RUnlock() + return len(fake.healthArgsForCall) +} + +func (fake *FakeSys) HealthCalls(stub func() (*api.HealthResponse, error)) { + fake.healthMutex.Lock() + defer fake.healthMutex.Unlock() + fake.HealthStub = stub +} + +func (fake *FakeSys) HealthReturns(result1 *api.HealthResponse, result2 error) { + fake.healthMutex.Lock() + defer fake.healthMutex.Unlock() + fake.HealthStub = nil + fake.healthReturns = struct { + result1 *api.HealthResponse + result2 error + }{result1, result2} +} + +func (fake *FakeSys) HealthReturnsOnCall(i int, result1 *api.HealthResponse, result2 error) { + fake.healthMutex.Lock() + defer fake.healthMutex.Unlock() + fake.HealthStub = nil + if fake.healthReturnsOnCall == nil { + fake.healthReturnsOnCall = make(map[int]struct { + result1 *api.HealthResponse + result2 error + }) + } + fake.healthReturnsOnCall[i] = struct { + result1 *api.HealthResponse + result2 error + }{result1, result2} +} + func (fake *FakeSys) ListMounts() (map[string]*api.MountOutput, error) { fake.listMountsMutex.Lock() ret, specificReturn := fake.listMountsReturnsOnCall[len(fake.listMountsArgsForCall)] @@ -231,6 +299,8 @@ func (fake *FakeSys) Invocations() map[string][][]interface{} { defer fake.invocationsMutex.RUnlock() fake.getPolicyMutex.RLock() defer fake.getPolicyMutex.RUnlock() + fake.healthMutex.RLock() + defer fake.healthMutex.RUnlock() fake.listMountsMutex.RLock() defer fake.listMountsMutex.RUnlock() fake.listPoliciesMutex.RLock() diff --git a/view/history.go b/view/history.go index 7a70544..e3ce8e3 100644 --- a/view/history.go +++ b/view/history.go @@ -1,8 +1,11 @@ package view +import "github.com/rs/zerolog" + type History struct { stack []func() HistorySize int + Logger *zerolog.Logger } func (h *History) push(back func()) { @@ -10,9 +13,12 @@ func (h *History) push(back func()) { if len(h.stack) > h.HistorySize { h.stack = h.stack[1:] } + h.Logger.Debug().Msgf("History stack: %v", h.stack) + } func (h *History) pop() { + h.Logger.Debug().Msgf("History stack: %v", h.stack) if len(h.stack) > 1 { last := h.stack[len(h.stack)-2] last() diff --git a/view/init.go b/view/init.go index 2823fca..2d0a3c1 100644 --- a/view/init.go +++ b/view/init.go @@ -18,11 +18,12 @@ func (v *View) Init(version string) { styles.StandardColorTag, styles.HighlightSecondaryTag, styles.StandardColorTag, - version, + v.state.VaultVersion, styles.HighlightSecondaryTag, v.state.Namespace, styles.StandardColorTag, ) + v.state.Version = version v.components.VaultInfo.Bind(v.Layout.Elements.ClusterInfo) v.components.VaultInfo.Render() @@ -104,7 +105,7 @@ func (v *View) Init(version string) { v.Layout.Pages.RemovePage(component.PageNameInfo) v.logger.Debug().Msgf("Info page removed, Active page is: %s", v.state.Elements.TableMain.GetTitle()) v.Layout.Container.SetFocus(v.state.Elements.TableMain) - // v.GoBack() + v.GoBack() } // Warn diff --git a/view/inputs.go b/view/inputs.go index e949394..8e11f35 100644 --- a/view/inputs.go +++ b/view/inputs.go @@ -33,7 +33,8 @@ func (v *View) InputMainCommands(event *tcell.EventKey) *tcell.EventKey { return event } switch event.Key() { - case tcell.KeyCtrlM: + // Bug: CTRL+M key maps to enter and causes conflicts + case tcell.KeyCtrlB: v.Watcher.Unsubscribe() v.Mounts() case tcell.KeyCtrlP: @@ -42,10 +43,6 @@ func (v *View) InputMainCommands(event *tcell.EventKey) *tcell.EventKey { // case tcell.KeyCtrlJ: // v.SecretObject() } - if event.Key() == tcell.KeyEnter { - v.logger.Debug().Msg("Enter pressed") - v.Layout.Container.SetFocus(v.state.Elements.TableMain) - } return event } diff --git a/view/mounts.go b/view/mounts.go index 3952529..f39b4a3 100644 --- a/view/mounts.go +++ b/view/mounts.go @@ -8,10 +8,12 @@ import ( ) func (v *View) Mounts() { + v.logger.Debug().Msg("Mounts view") v.viewSwitch() v.Layout.Body.SetTitle("Secret Mounts") v.Layout.Container.SetInputCapture(v.InputMounts) v.components.Commands.Update(component.MountsCommands) + v.state.Elements.TableMain = v.components.MountsTable.Table.Primitive().(*tview.Table) v.components.MountsTable.Logger = v.logger v.components.SecretsTable.Props.SelectedPath = "" v.state.SelectedMount = "" @@ -27,13 +29,15 @@ func (v *View) Mounts() { v.logger.Debug().Msgf("Selected path is: %v", v.state.SelectedPath) } - //v.Watcher.SubscribeToMounts(update) - v.Watcher.UpdateMounts() + v.Watcher.SubscribeToMounts(update) + // v.Watcher.UpdateMounts() update() - v.state.Elements.TableMain = v.components.MountsTable.Table.Primitive().(*tview.Table) + // Add this view to the history + v.addToHistory(v.state.SelectedNamespace, "mounts", func() { + v.Mounts() + }) v.Layout.Container.SetFocus(v.components.MountsTable.Table.Primitive()) - } func (v *View) parseMounts(data []*models.MountOutput) []*models.MountOutput { @@ -46,6 +50,12 @@ func (v *View) inputMounts(event *tcell.EventKey) *tcell.EventKey { } //todo switch event.Key() { + case tcell.KeyEnter: + if v.components.MountsTable.Table.Primitive().HasFocus() { + v.state.SelectedMount = v.components.MountsTable.GetIDForSelection() + v.Secrets("", "false") + return nil + } case tcell.KeyRune: switch event.Rune() { case 'e': diff --git a/view/policy.go b/view/policy.go index d859022..e8d008f 100644 --- a/view/policy.go +++ b/view/policy.go @@ -15,7 +15,7 @@ func (v *View) VPolicy() { v.Layout.Container.SetInputCapture(v.InputVaultPolicy) v.components.Commands.Update(component.PolicyCommands) search := v.components.Search - //table := v.components.PolicyTable + //table := v.components.in update := func() { if v.state.Toggle.Search { @@ -46,6 +46,12 @@ func (v *View) inputPolicy(event *tcell.EventKey) *tcell.EventKey { switch event.Key() { case tcell.KeyEsc: //v.GoBack() + case tcell.KeyEnter: + if v.components.PolicyTable.Table.Primitive().HasFocus() { + v.PolicyACL(v.components.PolicyTable.GetIDForSelection()) + v.Watcher.Unsubscribe() + return nil + } case tcell.KeyRune: switch event.Rune() { case '/': diff --git a/view/secretobj.go b/view/secretobj.go index d82fd5f..19a18e4 100644 --- a/view/secretobj.go +++ b/view/secretobj.go @@ -1,6 +1,8 @@ package view import ( + "strings" + "github.com/atotto/clipboard" "github.com/dkyanakiev/vaulty/component" "github.com/gdamore/tcell/v2" @@ -9,18 +11,19 @@ import ( func (v *View) SecretObject(mount, path string) { v.viewSwitch() - v.Layout.Body.SetTitle("Secret obejects") + v.Layout.Body.SetTitle("Secret object") v.Layout.Container.SetInputCapture(v.InputSecret) v.components.Commands.Update(component.SecretObjectCommands) - v.Layout.Container.SetFocus(v.components.SecretObjTable.Table.Primitive()) + v.logger.Debug().Msgf("Selected mount is: %v", mount) + v.logger.Debug().Msgf("Selected path is: %v", path) v.state.Elements.TableMain = v.components.SecretObjTable.Table.Primitive().(*tview.Table) v.components.SecretObjTable.Logger = v.logger v.components.SecretObjTable.Props.SelectedPath = path v.components.SecretObjTable.Props.ObscureSecrets = true update := func() { - + v.logger.Debug().Msgf("Focus set to %s", v.state.Elements.TableMain.GetTitle()) v.logger.Debug().Msgf("Selected path is: %v", v.state.SelectedPath) if !v.components.SecretObjTable.Editable { v.components.SecretObjTable.Render() @@ -34,6 +37,13 @@ func (v *View) SecretObject(mount, path string) { v.Watcher.SubscribeToSecret(mount, path, update) update() + v.state.Elements.TableMain = v.components.SecretObjTable.Table.Primitive().(*tview.Table) + v.Layout.Container.SetFocus(v.components.SecretObjTable.Table.Primitive()) + + v.addToHistory(v.state.SelectedNamespace, "secret", func() { + v.SecretObject(mount, path) + }) + } func (v *View) inputSecret(event *tcell.EventKey) *tcell.EventKey { @@ -62,15 +72,22 @@ func (v *View) inputSecret(event *tcell.EventKey) *tcell.EventKey { } } return nil + case 'b': + v.state.SelectedPath = strings.TrimSuffix(v.state.SelectedPath, "/") // Remove trailing slash + lastSlashIndex := strings.LastIndex(v.state.SelectedPath, "/") + if lastSlashIndex != -1 { + v.state.SelectedPath = v.state.SelectedPath[:lastSlashIndex+1] // Keep the slash + } else if v.state.SelectedPath != "" { + v.state.SelectedPath = "" // If no slash left and it's not empty, set to empty + v.components.SecretsTable.Props.SelectedPath = "" + } + v.Secrets(v.state.SelectedPath, "false") case 'j': v.components.SecretObjTable.ShowJson = !v.components.SecretObjTable.ShowJson v.components.SecretObjTable.ToggleView() case 'U': v.components.SecretObjTable.Editable = true v.components.SecretObjTable.ToggleView() - case 'o': - v.Layout.Pages.RemovePage(component.PageNameInfo) - v.Layout.Pages.RemovePage(component.PageNameError) default: if v.components.SecretObjTable.Editable { v.components.SecretObjTable.TextView.SetText(v.components.SecretObjTable.TextView.GetText(true) + string(event.Rune())) @@ -97,7 +114,16 @@ func (v *View) inputSecret(event *tcell.EventKey) *tcell.EventKey { case tcell.KeyEsc: v.components.SecretObjTable.Editable = false v.components.SecretObjTable.ToggleView() - return nil + v.state.SelectedPath = strings.TrimSuffix(v.state.SelectedPath, "/") // Remove trailing slash + lastSlashIndex := strings.LastIndex(v.state.SelectedPath, "/") + if lastSlashIndex != -1 { + v.state.SelectedPath = v.state.SelectedPath[:lastSlashIndex+1] // Keep the slash + } else if v.state.SelectedPath != "" { + v.state.SelectedPath = "" // If no slash left and it's not empty, set to empty + v.components.SecretsTable.Props.SelectedPath = "" + } + v.Secrets(v.state.SelectedPath, "false") + case tcell.KeyBackspace2, tcell.KeyBackspace: // If text editing is enabled, handle backspace if v.components.SecretObjTable.Editable { diff --git a/view/secrets.go b/view/secrets.go index ea66efd..0ace071 100644 --- a/view/secrets.go +++ b/view/secrets.go @@ -2,9 +2,12 @@ package view import ( "fmt" + "path/filepath" + "regexp" "strings" "github.com/dkyanakiev/vaulty/component" + "github.com/dkyanakiev/vaulty/models" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) @@ -19,6 +22,11 @@ func (v *View) Secrets(path string, secretBool string) { v.Layout.Body.SetTitle(fmt.Sprintf("Secrets: %s", path)) v.Layout.Container.SetInputCapture(v.InputSecrets) v.components.Commands.Update(component.SecretsCommands) + v.logger.Debug().Msgf("Selected path for secret is: %v", path) + + search := v.components.Search + v.state.Toggle.Search = false + v.state.Filter.Object = "" v.components.SecretsTable.Props.SelectedMount = v.state.SelectedMount if path != "" { @@ -26,11 +34,20 @@ func (v *View) Secrets(path string, secretBool string) { } update := func() { - v.components.SecretsTable.Props.Data = v.state.SecretsData + if v.state.Toggle.Search { + v.state.Filter.Object = v.FilterText + } + v.components.SecretsTable.Props.Data = v.filterSecrets() v.components.SecretsTable.Props.SelectedMount = v.state.SelectedMount v.components.SecretsTable.Render() v.Draw() + + } + + search.Props.ChangedFunc = func(text string) { + v.FilterText = text + update() } v.Watcher.SubscribeToSecrets(v.components.SecretsTable.Props.SelectedMount, @@ -48,6 +65,30 @@ func (v *View) inputSecrets(event *tcell.EventKey) *tcell.EventKey { } switch event.Key() { + case tcell.KeyEsc: + if v.components.SecretsTable.Table.Primitive().HasFocus() { + v.state.SelectedPath = strings.TrimSuffix(v.state.SelectedPath, "/") // Remove trailing slash + lastSlashIndex := strings.LastIndex(v.state.SelectedPath, "/") + if lastSlashIndex != -1 { + v.state.SelectedPath = v.state.SelectedPath[:lastSlashIndex+1] // Keep the slash + } else if v.state.SelectedPath != "" { + v.state.SelectedPath = "" // If no slash left and it's not empty, set to empty + v.components.SecretsTable.Props.SelectedPath = "" + } + v.Secrets(v.state.SelectedPath, "false") + } + case tcell.KeyEnter: + if v.components.SecretsTable.Table.Primitive().HasFocus() { + path, secretBool := v.components.SecretsTable.GetIDForSelection() + v.state.SelectedPath = fmt.Sprintf("%s%s", v.state.SelectedPath, path) + if secretBool == "true" { + v.SecretObject(v.state.SelectedMount, v.state.SelectedPath) + } else { + v.logger.Debug().Msgf("Running Secrets view with : %v", path) + v.Secrets(path, secretBool) + } + return nil + } case tcell.KeyRune: switch event.Rune() { case 'e': @@ -75,8 +116,42 @@ func (v *View) inputSecrets(event *tcell.EventKey) *tcell.EventKey { } v.Secrets(v.state.SelectedPath, "false") } + case '/': + if !v.Layout.Footer.HasFocus() { + if !v.state.Toggle.Search { + v.state.Toggle.Search = true + v.components.Search.InputField.SetText("") + v.Search() + } else { + v.Layout.Container.SetFocus(v.components.Search.InputField.Primitive()) + } + return nil + } } } return event } + +func (v *View) filterSecrets() []models.SecretPath { + data := v.state.SecretsData + filter := v.state.Filter.Object + if filter != "" { + rx, _ := regexp.Compile(filter) + var result []models.SecretPath + for _, p := range data { + switch true { + case rx.MatchString(p.PathName): + result = append(result, p) + } + } + return result + } + + return data +} + +func trimLastElement(s string) string { + dir, _ := filepath.Split(s) + return strings.TrimSuffix(dir, string(filepath.Separator)) + string(filepath.Separator) +} diff --git a/view/view.go b/view/view.go index 9b7c839..11a1955 100644 --- a/view/view.go +++ b/view/view.go @@ -79,6 +79,7 @@ func New(components *Components, watcher Watcher, client Client, state *state.St components: components, history: &History{ HistorySize: historySize, + Logger: logger, }, } } @@ -107,22 +108,22 @@ func (v *View) GoBack() { v.history.pop() } -// func (v *View) addToHistory(ns string, topic string, update func()) { -// v.history.push(func() { -// v.state.SelectedNamespace = ns -// // update() - -// // v.components.Selections.Props.Rerender = update -// v.components.Selections.Namespace.SetSelectedFunc(func(text string, index int) { -// v.state.SelectedNamespace = text -// update() -// }) -// // v.Watcher.Subscribe(topic, update) - -// index := getNamespaceNameIndex(ns, v.state.Namespaces) -// v.state.Elements.DropDownNamespace.SetCurrentOption(index) -// }) -// } +func (v *View) addToHistory(ns string, topic string, update func()) { + v.history.push(func() { + v.state.SelectedNamespace = ns + // update() + + // v.components.Selections.Props.Rerender = update + v.components.Selections.Namespace.SetSelectedFunc(func(text string, index int) { + v.state.SelectedNamespace = text + update() + }) + // v.Watcher.Subscribe(topic, update) + + //index := getNamespaceNameIndex(ns, v.state.Namespaces) + //v.state.Elements.DropDownNamespace.SetCurrentOption(index) + }) +} func (v *View) viewSwitch() { v.resetSearch()