├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── issue_report.md ├── .gitignore ├── .goreleaser.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── cmd └── vaul7y │ └── main.go ├── docs ├── examples │ └── vaul7y.yaml ├── local-setup.md └── usage.md ├── go.mod ├── go.sum ├── helpers └── setup.sh ├── images ├── screen1.png └── vaulty-min.gif ├── internal ├── config │ ├── configs.go │ └── logger.go ├── models │ └── models.go ├── state │ └── state.go ├── vault │ ├── client.go │ ├── helpers.go │ ├── kv.go │ ├── kv_test.go │ ├── logical.go │ ├── logical_test.go │ ├── mounts.go │ ├── mounts_test.go │ ├── namespaces.go │ ├── policy.go │ ├── policy_test.go │ ├── secret.go │ └── vaultfakes │ │ ├── fake_client.go │ │ ├── fake_kv2.go │ │ ├── fake_logical.go │ │ └── fake_sys.go └── watcher │ ├── activity.go │ ├── activity_test.go │ ├── mounts.go │ ├── mounts_test.go │ ├── namespaces.go │ ├── policies.go │ ├── policyacl.go │ ├── secretobj.go │ ├── secrets.go │ ├── watcher.go │ ├── watcher_test.go │ └── watcherfakes │ ├── fake_activities.go │ └── fake_vault.go └── tui ├── component ├── commands.go ├── commands_test.go ├── componentfakes │ ├── fake_box.go │ ├── fake_done_modal_func.go │ ├── fake_drop_down.go │ ├── fake_input_field.go │ ├── fake_modal.go │ ├── fake_selector.go │ ├── fake_table.go │ ├── fake_text_area.go │ └── fake_text_view.go ├── components.go ├── constants.go ├── error.go ├── error_test.go ├── info.go ├── info_test.go ├── jump.go ├── logo.go ├── logo_test.go ├── mounts_table.go ├── mounts_table_test.go ├── namespace_table.go ├── policy_acl_table.go ├── policy_acl_table_test.go ├── policy_table.go ├── policy_table_test.go ├── search.go ├── search_test.go ├── secret_obj_table.go ├── secret_obj_table_test.go ├── secrets_table.go ├── secrets_table_test.go ├── selections.go ├── selector.go ├── text_input.go ├── toggles.go ├── vaultinfo.go └── vaultinfo_test.go ├── layout ├── layout.go └── layout_test.go ├── primitives ├── box.go ├── dropdown.go ├── form.go ├── input.go ├── modal.go ├── selector.go ├── table.go ├── text.go └── textarea.go ├── styles └── styles.go └── view ├── handler.go ├── history.go ├── init.go ├── inputs.go ├── mounts.go ├── namespace.go ├── policy.go ├── policyacl.go ├── secretobj.go ├── secrets.go └── view.go /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea of a feature you would like 4 | title: '' 5 | labels: enhancement 6 | assignees: dkyanakiev 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe what it is.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: dkyanakiev 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' page 16 | 2. Enter '...' key strokes 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Please complete the following information:** 26 | - OS: [e.g. iOS, Windows] 27 | - Terminal [e.g. iterm2] 28 | - Version [e.g. v0.5.1 or how it was installed if "built from source"] 29 | - Vault cluster version. The full version string would indicate if its Ent or Oss version 30 | - Configuration options used, e.g. cli arguments, env vars, and/or `.vaul7y.yml` file 31 | - Vault permission policy (if applicable) 32 | 33 | **Additional context** 34 | Add any other context about the problem here. -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | .idea 23 | 24 | .DS_Store 25 | .vscode 26 | .vscode/* 27 | vendor/* 28 | bin 29 | bin/* 30 | pkg/ 31 | dist/ 32 | *.token 33 | *_token 34 | 35 | vaul7y-cfg.yml 36 | vaul7y-cfg.yaml -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sensible defaults. 2 | # Make sure to check the documentation at https://goreleaser.com 3 | 4 | # The lines below are called `modelines`. See `:help modeline` 5 | # Feel free to remove those if you don't want/need to use them. 6 | # yaml-language-server: $schema=https://goreleaser.com/static/schema.json 7 | # vim: set ts=2 sw=2 tw=0 fo=cnqoj 8 | 9 | project_name: vaul7y 10 | version: 1 11 | 12 | before: 13 | hooks: 14 | # You may remove this if you don't use go modules. 15 | - go mod tidy 16 | 17 | 18 | builds: 19 | - binary: vaul7y 20 | env: 21 | - CGO_ENABLED=0 22 | goos: 23 | - windows 24 | - darwin 25 | - linux 26 | goarch: 27 | - amd64 28 | - arm64 29 | goarm: 30 | - 6 31 | - 7 32 | ldflags: 33 | - -s -w -X "main.version={{ .Tag }}" 34 | dir: ./cmd/vaul7y/ 35 | 36 | 37 | archives: 38 | - name_template: >- 39 | {{ .ProjectName }}_{{ .Version }}_ 40 | {{- if eq .Os "darwin" }}Darwin 41 | {{- else if eq .Os "linux" }}Linux 42 | {{- else if eq .Os "windows" }}Windows 43 | {{- else }}{{ .Os }}{{ end }}_ 44 | {{- if eq .Arch "amd64" }}x86_64 45 | {{- else if eq .Arch "386" }}i386 46 | {{- else }}{{ .Arch }}{{ end }} 47 | 48 | checksum: 49 | name_template: 'checksums.txt' 50 | 51 | snapshot: 52 | name_template: "{{ incpatch .Version }}-next" 53 | 54 | changelog: 55 | sort: asc 56 | filters: 57 | exclude: 58 | - '^docs:' 59 | - '^test:' 60 | 61 | universal_binaries: 62 | - replace: true 63 | 64 | brews: 65 | - name: vaul7y 66 | homepage: https://github.com/dkyanakiev/vaul7y 67 | description: "A simple terminal application/TUI for interacting with HashiCorp Vault." 68 | folder: Formula 69 | commit_author: 70 | name: "Dimitar Yanakiev" 71 | email: "dkyanakiev@gmail.com" 72 | commit_msg_template: "Brew formula update for {{ .ProjectName }} version {{ .Tag }}" 73 | repository: 74 | owner: dkyanakiev 75 | name: homebrew-tap 76 | branch: main 77 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [0.1.9] - 2024-04-28 4 | 5 | ## Added 6 | 7 | -- Vault cache token lookup 8 | 9 | ## [0.1.8] - 2024-04-28 10 | 11 | ## Fixed 12 | 13 | -- Fixing issue with popups not being focused and requiring selection with mouse 14 | 15 | ## Added 16 | 17 | -- Adding metadata view on secret objects 18 | 19 | ## [0.1.7] - 2024-04-24 20 | 21 | ## Added 22 | 23 | -- Fallback method for mounts listing when user doesnt access to `sys/mounts` 24 | 25 | ## [0.1.5] - 2024-04-18 26 | 27 | ## Fixed 28 | 29 | - Fixing issue where vaulty will error if no config file is provided. 30 | 31 | ## [0.1.4] - 2024-04-17 32 | 33 | ## Fixed 34 | 35 | - Fixing issue where cli wont run if version check fails. 36 | 37 | ## [0.1.3] - 2024-03-14 38 | 39 | ## Fixed 40 | 41 | - Dynamic version passed when building the binary. 42 | 43 | ## [0.1.2] - 2024-03-13 44 | 45 | ## Added 46 | 47 | - Allows for custom config file to be passed over during using `-c` rather than using the default one 48 | 49 | ## [0.1.1] - 2024-01-24 50 | 51 | ## Added 52 | 53 | - Additional error message when failing to create vault client 54 | 55 | ## Fixed 56 | 57 | - Fixed loading for client key when using VAULT_CLIENT_KEY 58 | 59 | ## [0.1.0] - 2024-01-23 60 | 61 | ## Added 62 | - Env variable loading in addition to a yaml 63 | - Namespace support for enterprise vault instances 64 | 65 | ## Fixed 66 | - Minor bugfixes around navigation 67 | 68 | ## Changes 69 | - Housekeeping change 70 | 71 | ## [0.0.7] - 2023-12-03 72 | 73 | ## Added 74 | - Creation of new secrets and paths 75 | 76 | ## Fixed 77 | - Formatting and layout for different views around secrets when editing and displaying json 78 | 79 | ## Changes 80 | - Commands layout has `<` and `>` removed to improve readability 81 | 82 | 83 | ## [0.0.6] - 2023-12-03 84 | 85 | ## Added 86 | - Support for both PUT and PATCH for KV2 secrets 87 | - Had to modify the default methods in the vault package.. I couldn't figure out a clean way to get rid of the wrapper 88 | - Better key mappings 89 | - Additional information pane to show edit mode and filters used to search 90 | 91 | ## Fixed 92 | - Correctly scrolling to the top on secrets and policy view 93 | 94 | ## Changes 95 | - Refactoring and restructuring to make navigation in the repo easier 96 | 97 | ## [0.0.5] - 2023-11-30 98 | 99 | ### Fixed 100 | - Missing commands for 2 views 101 | - Version command check would fail if missing `VAULT_TOKEN` or `VAULT_ADDR` is missing 102 | 103 | ## [0.0.3] - 2023-11-30 104 | 105 | ### Added 106 | - Job filtering on secrets and mount views 107 | - Better navigation options between views 108 | - `vaul7y -v` to check the version 109 | - Added a check and error out to prevent vaul7y from freezing if vault token and address are not set 110 | 111 | ### Fixed 112 | - Error and Info modals tabbing out and changing focus 113 | - Enter key constantly moving you to the Secret Engines view. Its due to the way Unix system recognize Enter and Ctrl+M 114 | - Fixed an issue with watcher causing conflicts 115 | - Fixed logger to discard messages and not brake rendering when debugging is not enabled 116 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | local-vault: 3 | vault server -dev 4 | 5 | setup-test-data: 6 | ./helpers/setup.sh 7 | 8 | .PHONY: install-osx 9 | install-osx: 10 | cp ./bin/vaul7y/vaul7y /usr/local/bin/vaul7y 11 | 12 | .PHONY: dev 13 | dev: ## Build for the current development version 14 | @echo "==> Building Vaul7y..." 15 | @mkdir -p ./bin 16 | @CGO_ENABLED=0 go build -o ./bin/vaul7y ./cmd/vaul7y 17 | @rm -f $(GOPATH)/bin/vaul7y 18 | @cp ./bin/vaul7y/vaul7y $(GOPATH)/bin/vaul7y 19 | @echo "==> Done" 20 | 21 | .PHONY: build 22 | build: 23 | go build -ldflags "-X main.version=`git tag --sort=-version:refname | head -n 1`" -o bin/vaul7y ./cmd/vaul7y 24 | 25 | .PHONY: run 26 | run: 27 | ./bin/vaul7y 28 | 29 | .PHONY: test 30 | test: 31 | go test ./... -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vaul7y / Vaulty 2 | 3 | Vaulty is a TUI for Hashicorp Vault. The goal is to support as many functionalities as possible in order to make the tool as useful as possible. 4 | 5 | ## Why use Vaul7y 6 | 7 | I started the tool purely for personal use as I love tools like [K9s](https://github.com/derailed/k9s), [Wander](https://github.com/robinovitch61/wander) and [damon](https://github.com/hashicorp/damon). I generally prefer the use of CLI tools but when it came to vault and looking up at stuff, sometimes having a UI just speeds things up. I couldn't find something finished, so decided to write my own. 8 | 9 | ## Video 10 | ![gif](./images/vaulty-min.gif) 11 | 12 | ## Usage 13 | 14 | To see detailed guide on how to use the tool see the [docs](./docs/usage.md) 15 | 16 | ## Features and Bugs 17 | 18 | The tool is in active development and is bug heavy. There are multiple things that are on my short and long term TODO list. 19 | 20 | If anyone decides to use this and wants to request a specific feature or even fix a bug - [please open an issue](https://github.com/dkyanakiev/vaul7y/issues/new/choose). :smile: 21 | 22 | ## Short term TODO list: 23 | 1. [ ] Version select and rollback for secrets 24 | 2. [ ] Work on PKI and Certs 25 | -------------------------------------------------------------------------------- /cmd/vaul7y/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/dkyanakiev/vaulty/internal/config" 11 | "github.com/dkyanakiev/vaulty/internal/state" 12 | "github.com/dkyanakiev/vaulty/internal/vault" 13 | "github.com/dkyanakiev/vaulty/internal/watcher" 14 | "github.com/dkyanakiev/vaulty/tui/component" 15 | "github.com/dkyanakiev/vaulty/tui/view" 16 | "github.com/gdamore/tcell/v2" 17 | "github.com/jessevdk/go-flags" 18 | "github.com/rivo/tview" 19 | ) 20 | 21 | var version = "dev" 22 | 23 | type options struct { 24 | Version bool `short:"v" long:"version" description:"Show Damon version"` 25 | ConfigFile string `short:"c" long:"config" description:"Path to the config file"` 26 | } 27 | 28 | func main() { 29 | 30 | var opts options 31 | _, err := flags.ParseArgs(&opts, os.Args) 32 | if err != nil { 33 | os.Exit(1) 34 | } 35 | 36 | if opts.Version { 37 | fmt.Println("vaul7y", strings.TrimPrefix(version, "v")) 38 | os.Exit(0) 39 | } 40 | 41 | // Check for required Vault env vars 42 | cfg := config.LoadConfig(opts.ConfigFile) 43 | 44 | logFile, logger := config.SetupLogger(cfg.VaultyLogLevel, cfg.VaultyLogFile) 45 | defer logFile.Close() 46 | tview.Styles.PrimitiveBackgroundColor = tcell.NewRGBColor(40, 44, 48) 47 | 48 | vaultClient, err := vault.New(func(v *vault.Vault) error { 49 | return vault.Default(v, logger, cfg) 50 | }) 51 | if err != nil { 52 | fmt.Printf("Failed to start Vault client: %v\n", err) 53 | os.Exit(1) 54 | } 55 | 56 | refreshIntervalDefault := time.Duration(cfg.VaultyRefreshRate) * time.Second 57 | state := initializeState(vaultClient, cfg.VaultNamespace) 58 | toggles := component.NewTogglesInfo() 59 | selections := component.NewSelections(state) 60 | namespaces := component.NewNamespaceTable() 61 | commands := component.NewCommands() 62 | vaultInfo := component.NewVaultInfo() 63 | mounts := component.NewMountsTable() 64 | policies := component.NewPolicyTable() 65 | policyAcl := component.NewPolicyAclTable() 66 | secrets := component.NewSecretsTable() 67 | secretObj := component.NewSecretObjTable() 68 | logo := component.NewLogo(version) 69 | info := component.NewInfo() 70 | failure := component.NewInfo() 71 | errorComp := component.NewError() 72 | components := &view.Components{ 73 | VaultInfo: vaultInfo, 74 | Commands: commands, 75 | Selections: selections, 76 | NamespaceTable: namespaces, 77 | MountsTable: mounts, 78 | PolicyTable: policies, 79 | PolicyAclTable: policyAcl, 80 | SecretsTable: secrets, 81 | SecretObjTable: secretObj, 82 | Info: info, 83 | Error: errorComp, 84 | Failure: failure, 85 | Logo: logo, 86 | Logger: logger, 87 | TogglesInfo: toggles, 88 | } 89 | watcher := watcher.NewWatcher(state, vaultClient, refreshIntervalDefault, logger) 90 | view := view.New(components, watcher, vaultClient, state, logger) 91 | view.Init(version) 92 | 93 | //view.Init("0.0.1") 94 | err = view.Layout.Container.Run() 95 | if err != nil { 96 | log.Fatal("cannot initialize view.") 97 | } 98 | 99 | } 100 | 101 | func initializeState(client *vault.Vault, rootNs string) *state.State { 102 | state := state.New() 103 | addr := client.Address() 104 | version := client.Version 105 | state.VaultAddress = addr 106 | state.VaultVersion = version 107 | state.DefaultNamespace = "-" 108 | state.RootNamespace = "-" 109 | 110 | if rootNs != "" { 111 | state.Enterprise = true 112 | state.RootNamespace = getFirstPart(rootNs) 113 | state.DefaultNamespace = rootNs 114 | state.SelectedNamespace = rootNs 115 | state.Namespaces, _ = client.ListNamespaces() 116 | } 117 | 118 | return state 119 | } 120 | 121 | func getFirstPart(s string) string { 122 | parts := strings.Split(s, "/") 123 | if len(parts) > 0 { 124 | return parts[0] 125 | } 126 | return "" 127 | } 128 | -------------------------------------------------------------------------------- /docs/examples/vaul7y.yaml: -------------------------------------------------------------------------------- 1 | #r equired 2 | vault_addr: http://127.0.0.1:8200 3 | vault_token: hvs.token 4 | # For ent users only 5 | vault_namespace: admin 6 | vault_ca_cert: /tmp/ca.crt 7 | vault_client_cert: /tmp/client.crt 8 | vault_client_key: /tmp/client.key 9 | # optional 10 | vaulty_log_file: /tmp/my-vault-log.log 11 | vaulty_log_level: debug 12 | # How often to pull information from Vault. Added in case rate limit is a problem 13 | # Default is 30 seconds 14 | # vaulty_refresh_rate: 30 -------------------------------------------------------------------------------- /docs/local-setup.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | The only real component for a dev setup is a vault server running 4 | 1. Simply run vault in dev mode 5 | ``` 6 | $ vault server -dev 7 | ``` 8 | 9 | 2. The make file currently has some generation for mock data in order to test features. Will be updated as more features are added. 10 | ``` 11 | $ make setup-test-data 12 | ``` 13 | * Note: This requires Vault tokent to be set in order to be able to write to Vault 14 | 15 | 3. Configure the env variables required to auth to vault or `.vaul7y.yaml` in your home directory 16 | 17 | 4. Make sure to set 18 | `VAULTY_LOG_FILE` env variable and point to a file, to log to a file 19 | `VAULTY_LOG_LEVEL` env variable - define the log level you want to use 20 | 21 | ``` 22 | ❯ export VAULTY_LOG_LEVEL=debug 23 | ❯ export VAULTY_LOG_FILE=/tmp/my-vault-log.log 24 | ``` -------------------------------------------------------------------------------- /docs/usage.md: -------------------------------------------------------------------------------- 1 | # Usage 2 | 3 | ## Setup 4 | ### Installation 5 | 6 | ### Brew 7 | 8 | ```shell 9 | brew install dkyanakiev/tap/vaul7y 10 | ``` 11 | to upgrade 12 | ```shell 13 | brew update && brew upgrade vaul7y 14 | ``` 15 | 16 | ### Download from GitHub 17 | 18 | Download the relevant binary for your operating system (macOS = Darwin) from 19 | the [latest Github release](https://github.com/dkyanakiev/vaul7y/releases). Unpack it, then move the binary to 20 | somewhere accessible in your `PATH`, e.g. `mv ./vaul7y /usr/local/bin`. 21 | 22 | ### > Using [go installed on your machine](https://go.dev/doc/install) 23 | 24 | ```shell 25 | go install github.com/dkyanakiev/vaul7y@latest 26 | ``` 27 | 28 | ### Building from source and Run Vaul7y 29 | 30 | Make sure you have your go environment setup: 31 | 32 | 1. Clone the project 33 | 1. Run `$ make build` to build the binary 34 | 1. Run `$ make run` to run the binary 35 | 1. You can use `$ make install-osx` on a Mac to cp the binary to `/usr/local/bin/vaul7y` 36 | 37 | or 38 | 39 | ``` 40 | $ go install ./cmd/vaul7y 41 | ``` 42 | 43 | ### How to use it 44 | 45 | Once `Vaul7y` is installed and avialable in your path, simply run: 46 | 47 | ``` 48 | $ vaul7y 49 | ``` 50 | 51 | ![image](../images/screen1.png) 52 | 53 | 54 | ### Environment variables 55 | 56 | In order to use the tool you must expose the needed env variables, that would generally be used by the vault cli to auth to a given cluster. 57 | 58 | Required: 59 | `VAULT_ADDR` 60 | `VAULT_TOKEN` 61 | 62 | For the full list see the [official docs](https://developer.hashicorp.com/vault/docs/commands#environment-variables) 63 | 64 | Another option is to store your configs in yaml file named `.vaul7y.yaml` stored in your home directory. 65 | Example: [`~/myuser/.vaul7y.yaml`](./examples/vaul7y.yaml) 66 | 67 | Or alternatively pass a config file as an argument using `-c ` 68 | Example: `vaul7y -c ./new-env.yml` 69 | 70 | #### Authentication and variables priority 71 | Variables will be loaded in the following order, with the next superseding the previous ones: 72 | 73 | 1. Will check for vault [token cache](https://developer.hashicorp.com/vault/docs/commands#authenticating-to-vault) 74 | 2. Read from env variables 75 | 3. Config file 76 | 77 | ### Features 78 | 79 | Currently the capabilities are limited. 80 | 81 | * Support for navigation between KV mounts 82 | * Currently only KV2 83 | * Looking up secret objects 84 | * Show/hide secrets and coping data 85 | * Update/patch secrets 86 | * Create new secrets 87 | * Filter paths/secrets 88 | * Support for exploring and filtering ACL Policies 89 | * Namespace support for Enteprise versions 90 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dkyanakiev/vaulty 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.3 6 | 7 | require ( 8 | github.com/atotto/clipboard v0.1.4 9 | github.com/gdamore/tcell/v2 v2.6.0 10 | github.com/hashicorp/vault/api v1.12.2 11 | github.com/jessevdk/go-flags v1.5.0 12 | github.com/mitchellh/mapstructure v1.5.0 13 | github.com/rivo/tview v0.0.0-20230907083354-a39fe28ba466 14 | github.com/rs/zerolog v1.31.0 15 | github.com/stretchr/testify v1.8.4 16 | gopkg.in/yaml.v2 v2.4.0 17 | ) 18 | 19 | require ( 20 | github.com/cenkalti/backoff/v3 v3.0.0 // indirect 21 | github.com/davecgh/go-spew v1.1.1 // indirect 22 | github.com/gdamore/encoding v1.0.0 // indirect 23 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 24 | github.com/go-test/deep v1.0.3 // indirect 25 | github.com/hashicorp/errwrap v1.1.0 // indirect 26 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 27 | github.com/hashicorp/go-hclog v1.2.0 // indirect 28 | github.com/hashicorp/go-multierror v1.1.1 // indirect 29 | github.com/hashicorp/go-retryablehttp v0.6.6 // indirect 30 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 31 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 // indirect 32 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 33 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/kr/pretty v0.3.1 // indirect 36 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 37 | github.com/mattn/go-colorable v0.1.13 // indirect 38 | github.com/mattn/go-isatty v0.0.20 // indirect 39 | github.com/mattn/go-runewidth v0.0.15 // indirect 40 | github.com/mitchellh/go-homedir v1.1.0 // indirect 41 | github.com/pmezard/go-difflib v1.0.0 // indirect 42 | github.com/rivo/uniseg v0.4.3 // indirect 43 | github.com/ryanuber/go-glob v1.0.0 // indirect 44 | golang.org/x/crypto v0.19.0 // indirect 45 | golang.org/x/net v0.19.0 // indirect 46 | golang.org/x/sys v0.17.0 // indirect 47 | golang.org/x/term v0.17.0 // indirect 48 | golang.org/x/text v0.14.0 // indirect 49 | golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect 50 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 51 | gopkg.in/yaml.v3 v3.0.1 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /helpers/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set Vault address 4 | export VAULT_ADDR='http://127.0.0.1:8200' 5 | 6 | # Login to Vault 7 | vault login $VAULT_TOKEN 8 | 9 | # # Create multiple KV v2 stores with random names 10 | # for i in {1..10} 11 | # do 12 | # kv_store_name="kv$(uuidgen | cut -c1-8)" 13 | # vault secrets enable -version=2 -path=$kv_store_name kv 14 | # done 15 | 16 | # Create random secrets in each KV store 17 | for kv_store_name in $(vault secrets list -format=json | jq -r 'to_entries[] | select(.value.type == "kv") | .key') 18 | do 19 | for j in {1..100} 20 | do 21 | # Create a secret at the root of the KV store 22 | vault kv put $kv_store_name/data/secret$j key1=$(openssl rand -base64 12) key2=$(openssl rand -base64 12) 23 | 24 | # Create a nested object in the KV store 25 | nested_object_name="object$(uuidgen | cut -c1-8)" 26 | for k in {1..5} 27 | do 28 | # Create a secret in the nested object 29 | vault kv put $kv_store_name/data/$nested_object_name/secret$k key1=$(openssl rand -base64 12) key2=$(openssl rand -base64 12) 30 | done 31 | done 32 | done 33 | 34 | # Number of policies to create 35 | num_policies=10 36 | 37 | # Base name for the policies 38 | policy_base_name="random_policy" 39 | 40 | # Base path for the policies 41 | policy_base_path="secret/data/random" 42 | 43 | for ((i=1; i<=num_policies; i++)); do 44 | # Generate a random path 45 | random_path="$policy_base_path/$RANDOM" 46 | 47 | # Create a temporary file for the policy 48 | policy_file=$(mktemp) 49 | 50 | # Write the policy to the temporary file 51 | echo "path \"$random_path\" { capabilities = [\"read\"] }" > $policy_file 52 | 53 | # Create the policy in Vault 54 | vault policy write "$policy_base_name$i" $policy_file 55 | 56 | # Remove the temporary file 57 | rm $policy_file 58 | done -------------------------------------------------------------------------------- /images/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyanakiev/vaul7y/16109869168d0e459fd6841de1682287425653a1/images/screen1.png -------------------------------------------------------------------------------- /images/vaulty-min.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkyanakiev/vaul7y/16109869168d0e459fd6841de1682287425653a1/images/vaulty-min.gif -------------------------------------------------------------------------------- /internal/config/configs.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/signal" 8 | "path/filepath" 9 | "runtime" 10 | "strconv" 11 | "strings" 12 | "syscall" 13 | 14 | "gopkg.in/yaml.v2" 15 | ) 16 | 17 | type Config struct { 18 | VaultAddr string `yaml:"vault_addr"` 19 | VaultNamespace string `yaml:"vault_namespace"` 20 | VaultToken string `yaml:"vault_token"` 21 | VaultCaCert string `yaml:"vault_cacert"` 22 | VaultClientCert string `yaml:"vault_client_cert"` 23 | VaultClientKey string `yaml:"vault_client_key"` 24 | VaultyLogFile string `yaml:"vaulty_log_file"` 25 | VaultyLogLevel string `yaml:"vaulty_log_level"` 26 | VaultyRefreshRate int `yaml:"vaulty_refresh_rate"` 27 | } 28 | 29 | func LoadConfig(cfgFile string) Config { 30 | var config Config 31 | // Load the config from the YAML file 32 | home, err := os.UserHomeDir() 33 | if err != nil { 34 | fmt.Println("Error getting user home directory") 35 | } 36 | 37 | var data []byte 38 | if cfgFile == "" { 39 | yamlFilePath := filepath.Join(home, ".vaul7y.yaml") 40 | if _, err := os.Stat(yamlFilePath); os.IsNotExist(err) { 41 | fmt.Printf("Config file does not exist: %s\n", yamlFilePath) 42 | } else { 43 | data, err = os.ReadFile(yamlFilePath) 44 | if err != nil { 45 | fmt.Printf("Error reading YAML file: %v\n", err) 46 | } 47 | } 48 | } else { 49 | fmt.Println("Using config file: ", cfgFile) 50 | data, err = os.ReadFile(cfgFile) 51 | if err != nil { 52 | fmt.Printf("Error reading YAML file: %v\n", err) 53 | } 54 | } 55 | 56 | if data != nil { 57 | err = yaml.Unmarshal(data, &config) 58 | if err != nil { 59 | fmt.Printf("Error parsing YAML file: %v\n", err) 60 | } 61 | } 62 | 63 | // Check for vault cache 64 | home, err = os.UserHomeDir() 65 | if err != nil { 66 | fmt.Println("Error getting user home directory") 67 | } else { 68 | vaultTokenPath := filepath.Join(home, ".vault-token") 69 | if _, err := os.Stat(vaultTokenPath); os.IsNotExist(err) { 70 | fmt.Printf("Vault token file does not exist: %s\n", vaultTokenPath) 71 | } else { 72 | data, err := os.ReadFile(vaultTokenPath) 73 | if err != nil { 74 | fmt.Printf("Error reading vault token file: %v\n", err) 75 | } else { 76 | config.VaultToken = string(data) 77 | } 78 | } 79 | } 80 | 81 | // Overwrite with environment variables if they are set 82 | if vaultAddr := os.Getenv("VAULT_ADDR"); vaultAddr != "" { 83 | config.VaultAddr = vaultAddr 84 | } 85 | if vaultNamespace := os.Getenv("VAULT_NAMESPACE"); vaultNamespace != "" { 86 | config.VaultNamespace = vaultNamespace 87 | } 88 | if vaultToken := os.Getenv("VAULT_TOKEN"); vaultToken != "" { 89 | config.VaultToken = vaultToken 90 | } 91 | if vaultCaCert := os.Getenv("VAULT_CACERT"); vaultCaCert != "" { 92 | config.VaultCaCert = vaultCaCert 93 | } 94 | if vaultClientCert := os.Getenv("VAULT_CLIENT_CERT"); vaultClientCert != "" { 95 | config.VaultClientCert = vaultClientCert 96 | } 97 | if vaultClientKey := os.Getenv("VAULT_CLIENT_KEY"); vaultClientKey != "" { 98 | config.VaultClientKey = vaultClientKey 99 | } 100 | if vaultyLogFile := os.Getenv("VAULTY_LOG_FILE"); vaultyLogFile != "" { 101 | config.VaultyLogFile = vaultyLogFile 102 | } 103 | if vaultyLogLevel := os.Getenv("VAULTY_LOG_LEVEL"); vaultyLogLevel != "" { 104 | config.VaultyLogLevel = vaultyLogLevel 105 | } 106 | if vaultyRefreshRate := os.Getenv("VAULTY_REFRESH_RATE"); vaultyRefreshRate != "" { 107 | vaultyRefreshRateInt, err := strconv.Atoi(vaultyRefreshRate) 108 | if err != nil { 109 | fmt.Printf("Error converting VAULTY_REFRESH_RATE to int: %v", err) 110 | } else { 111 | config.VaultyRefreshRate = vaultyRefreshRateInt 112 | } 113 | } 114 | 115 | if config.VaultToken == "" { 116 | home, err := os.UserHomeDir() 117 | if err != nil { 118 | fmt.Println("Error getting user home directory") 119 | } else { 120 | vaultTokenPath := filepath.Join(home, ".vault-token") 121 | if _, err := os.Stat(vaultTokenPath); err == nil { 122 | data, err := os.ReadFile(vaultTokenPath) 123 | if err != nil { 124 | fmt.Printf("Error reading vault token file: %v\n", err) 125 | } else { 126 | config.VaultToken = string(data) 127 | } 128 | } 129 | } 130 | } 131 | 132 | if config.VaultAddr == "" { 133 | fmt.Println("VAULT_ADDR is not set. Please set it and try again.") 134 | os.Exit(1) 135 | } 136 | 137 | if config.VaultToken == "" { 138 | fmt.Println("VAULT_TOKEN is not set. Please set it and try again.") 139 | os.Exit(1) 140 | } 141 | 142 | if config.VaultyRefreshRate == 0 { 143 | config.VaultyRefreshRate = 30 144 | } 145 | 146 | if strings.EqualFold(config.VaultyLogLevel, "debug") { 147 | go func() { 148 | ch := make(chan os.Signal, 1) 149 | signal.Notify(ch, syscall.SIGTERM) 150 | 151 | <-ch 152 | fmt.Println("Dumping goroutines") 153 | bufsize := int(10 * 1024 * 1024) // 10 MiB 154 | buf := make([]byte, bufsize) 155 | n := runtime.Stack(buf, true) 156 | filename := fmt.Sprintf("%s.dump", config.VaultyLogFile) 157 | 158 | ioutil.WriteFile(filename, buf[:n], 0644) 159 | os.Exit(1) 160 | }() 161 | 162 | } 163 | 164 | return config 165 | } 166 | -------------------------------------------------------------------------------- /internal/config/logger.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | "github.com/rs/zerolog" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | func SetupLogger(logLevel string, logFileName string) (*os.File, *zerolog.Logger) { 12 | // UNIX Time is faster and smaller than most timestamps 13 | zerolog.TimeFieldFormat = zerolog.TimeFormatUnix 14 | 15 | var logger zerolog.Logger 16 | 17 | // Default level for this example is info, unless debug flag is present 18 | if strings.EqualFold(logLevel, "debug") { 19 | level, err := zerolog.ParseLevel(logLevel) 20 | if err != nil { 21 | log.Fatal().Err(err).Msg("Invalid log level") 22 | } 23 | zerolog.SetGlobalLevel(level) 24 | logger = zerolog.New(os.Stdout).With().Timestamp().Logger() 25 | } else { 26 | // If debugOn is false, discard all log messages 27 | logger = zerolog.Nop() 28 | } 29 | 30 | var logFile *os.File 31 | 32 | // Check if file for logging is set 33 | 34 | if logFileName != "" { 35 | logFile, err := os.OpenFile(logFileName, os.O_APPEND|os.O_RDWR|os.O_CREATE, 0644) 36 | if err != nil { 37 | log.Panic().Err(err).Msg("Error opening log file") 38 | } 39 | logger = logger.Output(zerolog.ConsoleWriter{Out: logFile, TimeFormat: zerolog.TimeFieldFormat}) 40 | } 41 | 42 | return logFile, &logger 43 | } 44 | -------------------------------------------------------------------------------- /internal/state/state.go: -------------------------------------------------------------------------------- 1 | package state 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/internal/models" 5 | "github.com/hashicorp/vault/api" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | type State struct { 10 | VaultAddress string 11 | VaultVersion string 12 | Mounts map[string]*models.MountOutput 13 | SecretsData []models.SecretPath 14 | KV2 []models.KVSecret 15 | // Central/Root ns for the Vault instance 16 | RootNamespace string 17 | DefaultNamespace string 18 | Namespaces []string 19 | // Current ns for the Vault instance 20 | SelectedNamespace string 21 | SelectedMount string 22 | SelectedPath string 23 | SelectedObject string 24 | SelectedPolicyName string 25 | SelectedSecret *api.Secret 26 | SelectedSecretMeta *models.Metadata 27 | PolicyList []string 28 | PolicyACL string 29 | NewSecretName string 30 | Enterprise bool 31 | 32 | Elements *Elements 33 | Toggle *Toggle 34 | Filter *Filter 35 | Version string 36 | } 37 | 38 | type Toggle struct { 39 | Search bool 40 | JumpToPolicy bool 41 | TextInput bool 42 | } 43 | 44 | type Filter struct { 45 | Object string 46 | Policy string 47 | Namespace string 48 | } 49 | 50 | type Elements struct { 51 | DropDownNamespace *tview.DropDown 52 | TableMain *tview.Table 53 | TextMain *tview.TextView 54 | } 55 | 56 | func New() *State { 57 | return &State{ 58 | Elements: &Elements{}, 59 | Toggle: &Toggle{}, 60 | Filter: &Filter{}, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/vault/client.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/config" 7 | "github.com/dkyanakiev/vaulty/internal/models" 8 | "github.com/hashicorp/vault/api" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | //go:generate counterfeiter . Client 13 | type Client interface { 14 | Address() string 15 | } 16 | 17 | type Vault struct { 18 | vault *api.Client 19 | Client Client 20 | KV2 KV2 21 | Sys Sys 22 | NsClient NamespaceClient 23 | Logical Logical 24 | Secret Secret 25 | Logger *zerolog.Logger 26 | Version string 27 | } 28 | 29 | //go:generate counterfeiter . Logical 30 | type Logical interface { 31 | List(path string) (*api.Secret, error) 32 | } 33 | 34 | type Secret interface { 35 | //ListSecrets(string) (*api.Secret, error) 36 | } 37 | 38 | //go:generate counterfeiter . Sys 39 | type Sys interface { 40 | ListMounts() (map[string]*api.MountOutput, error) 41 | ListPolicies() ([]string, error) 42 | GetPolicy(name string) (string, error) 43 | Health() (*api.HealthResponse, error) 44 | // ListNamespaces() ([]models.Namespace, error) 45 | //ListMounts() ([]*api.Sys, error) 46 | } 47 | 48 | //go:generate counterfeiter . KV2 49 | type KV2 interface { 50 | Get(context.Context, string) (*api.KVSecret, error) 51 | GetMetadata(context.Context, string) (*api.KVMetadata, error) 52 | Patch(context.Context, string, map[string]interface{}, ...KVOption) (*api.KVSecret, error) 53 | Put(context.Context, string, map[string]interface{}, ...KVOption) (*api.KVSecret, error) 54 | 55 | // GetVersion(context.Context, string, int) (*api.KVSecret, error) 56 | // GetVersionsAsList(context.Context, string) ([]*api.KVVersionMetadata, error) 57 | } 58 | 59 | type NamespaceClient interface { 60 | List() ([]*models.Namespace, error) 61 | } 62 | 63 | type KVOption func() (key string, value interface{}) 64 | 65 | func New(opts ...func(*Vault) error) (*Vault, error) { 66 | vault := Vault{} 67 | for _, opt := range opts { 68 | err := opt(&vault) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | return &vault, nil 75 | } 76 | 77 | func Default(v *Vault, log *zerolog.Logger, vaultyCfg config.Config) error { 78 | cfg := api.DefaultConfig() 79 | cfg.Address = vaultyCfg.VaultAddr 80 | 81 | tlsCfg := &api.TLSConfig{} 82 | 83 | if vaultyCfg.VaultCaCert != "" { 84 | tlsCfg.CACert = vaultyCfg.VaultCaCert 85 | } 86 | 87 | if vaultyCfg.VaultClientCert != "" { 88 | tlsCfg.ClientCert = vaultyCfg.VaultClientCert 89 | } 90 | 91 | if vaultyCfg.VaultClientKey != "" { 92 | tlsCfg.ClientKey = vaultyCfg.VaultClientKey 93 | } 94 | 95 | err := cfg.ConfigureTLS(tlsCfg) 96 | if err != nil { 97 | log.Error().Err(err).Msgf("Failed to configure TLS: %v", err) 98 | return err 99 | } 100 | 101 | client, err := api.NewClient(cfg) 102 | if err != nil { 103 | log.Error().Err(err).Msg("Failed to create Vault client") 104 | return err 105 | } 106 | 107 | client.SetToken(vaultyCfg.VaultToken) 108 | var version string 109 | // Check if the client is successfully created by making a request to Vault 110 | health, err := client.Sys().Health() 111 | if err != nil { 112 | log.Error().Err(err).Msg("Failed to connect to `v1/sys/health` ") 113 | } else { 114 | version = health.Version 115 | } 116 | // Check for enterprise version and set namespace 117 | if vaultyCfg.VaultNamespace != "" { 118 | client.SetNamespace(vaultyCfg.VaultNamespace) 119 | } 120 | 121 | log.Debug().Msg("Vault client successfully created and connected") 122 | 123 | v.vault = client 124 | v.Client = client 125 | v.Sys = client.Sys() 126 | v.Logical = client.Logical() 127 | v.Version = version 128 | v.Logger = log 129 | 130 | return nil 131 | } 132 | 133 | func (v *Vault) Address() string { 134 | return v.Client.Address() 135 | } 136 | 137 | // func (v *Vault) Version() (string, error) { 138 | // health, err := v.Sys.Health() 139 | // if err != nil { 140 | // return "", err 141 | // } 142 | // return health.Version, nil 143 | // } 144 | -------------------------------------------------------------------------------- /internal/vault/helpers.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "reflect" 9 | "strings" 10 | 11 | "github.com/hashicorp/vault/api" 12 | ) 13 | 14 | // extractListData reads the secret and returns a typed list of data and a 15 | // boolean indicating whether the extraction was successful. 16 | func extractListData(secret *api.Secret) ([]interface{}, bool) { 17 | if secret == nil || secret.Data == nil { 18 | return nil, false 19 | } 20 | 21 | k, ok := secret.Data["keys"] 22 | if !ok || k == nil { 23 | return nil, false 24 | } 25 | 26 | i, ok := k.([]interface{}) 27 | return i, ok 28 | } 29 | 30 | // func OutputList(ui cli.Ui, data interface{}) int { 31 | // switch data := data.(type) { 32 | // case *api.Secret: 33 | // secret := data 34 | // return outputWithFormat(ui, secret, secret.Data["keys"]) 35 | // default: 36 | // return outputWithFormat(ui, nil, data) 37 | // } 38 | // } 39 | 40 | func ParseSecret(r io.Reader) (*api.Secret, error) { 41 | // First read the data into a buffer. Not super efficient but we want to 42 | // know if we actually have a body or not. 43 | var buf bytes.Buffer 44 | 45 | // io.Reader is treated like a stream and cannot be read 46 | // multiple times. Duplicating this stream using TeeReader 47 | // to use this data in case there is no top-level data from 48 | // api response 49 | var teebuf bytes.Buffer 50 | tee := io.TeeReader(r, &teebuf) 51 | 52 | _, err := buf.ReadFrom(tee) 53 | if err != nil { 54 | return nil, err 55 | } 56 | if buf.Len() == 0 { 57 | return nil, nil 58 | } 59 | 60 | // First decode the JSON into a map[string]interface{} 61 | var secret api.Secret 62 | dec := json.NewDecoder(&buf) 63 | dec.UseNumber() 64 | if err := dec.Decode(&secret); err != nil { 65 | return nil, err 66 | } 67 | 68 | // If the secret is null, add raw data to secret data if present 69 | if reflect.DeepEqual(secret, api.Secret{}) { 70 | data := make(map[string]interface{}) 71 | dec := json.NewDecoder(&teebuf) 72 | dec.UseNumber() 73 | if err := dec.Decode(&data); err != nil { 74 | return nil, err 75 | } 76 | errRaw, errPresent := data["errors"] 77 | 78 | // if only errors are present in the resp.Body return nil 79 | // to return value not found as it does not have any raw data 80 | if len(data) == 1 && errPresent { 81 | return nil, nil 82 | } 83 | 84 | // if errors are present along with raw data return the error 85 | if errPresent { 86 | var errStrArray []string 87 | errBytes, err := json.Marshal(errRaw) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if err := json.Unmarshal(errBytes, &errStrArray); err != nil { 92 | return nil, err 93 | } 94 | return nil, fmt.Errorf(strings.Join(errStrArray, " ")) 95 | } 96 | 97 | // if any raw data is present in resp.Body, add it to secret 98 | if len(data) > 0 { 99 | secret.Data = data 100 | } 101 | } 102 | 103 | return &secret, nil 104 | } 105 | func DataIterator(t interface{}) { 106 | switch reflect.TypeOf(t).Kind() { 107 | case reflect.Slice: 108 | s := reflect.ValueOf(t) 109 | 110 | for i := 0; i < s.Len(); i++ { 111 | fmt.Println(s.Index(i)) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /internal/vault/kv.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/hashicorp/vault/api" 7 | ) 8 | 9 | func (v *Vault) Get(ctx context.Context, path string) (*api.KVSecret, error) { 10 | secret, err := v.KV2.Get(ctx, path) 11 | if err != nil { 12 | v.Logger.Err(err).Msgf("filed to retrieve secret: %s", err) 13 | } 14 | return secret, nil 15 | } 16 | 17 | func (v *Vault) GetMetadata(ctx context.Context, path string) (*api.KVMetadata, error) { 18 | secret, err := v.KV2.GetMetadata(ctx, path) 19 | if err != nil { 20 | v.Logger.Err(err).Msgf("filed to retrieve secret: %s", err) 21 | } 22 | return secret, nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/vault/kv_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/vault" 8 | "github.com/dkyanakiev/vaulty/internal/vault/vaultfakes" 9 | "github.com/hashicorp/vault/api" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGet(t *testing.T) { 14 | ctx := context.Background() 15 | path := "testpath" 16 | 17 | fakeKV2 := &vaultfakes.FakeKV2{} 18 | 19 | fakeKV2.GetReturns(&api.KVSecret{}, nil) 20 | 21 | v := &vault.Vault{ 22 | KV2: fakeKV2, 23 | } 24 | 25 | secret, err := v.Get(ctx, path) 26 | 27 | assert.NoError(t, err) 28 | assert.NotNil(t, secret) 29 | fakeKV2.Get(ctx, path) 30 | 31 | } 32 | 33 | func TestGetMetadata(t *testing.T) { 34 | ctx := context.Background() 35 | path := "testpath" 36 | 37 | fakeKV2 := &vaultfakes.FakeKV2{} 38 | 39 | fakeKV2.GetMetadataReturns(&api.KVMetadata{}, nil) 40 | 41 | v := &vault.Vault{ 42 | KV2: fakeKV2, 43 | } 44 | 45 | secret, err := v.GetMetadata(ctx, path) 46 | 47 | assert.NoError(t, err) 48 | assert.NotNil(t, secret) 49 | fakeKV2.GetMetadata(ctx, path) 50 | } 51 | -------------------------------------------------------------------------------- /internal/vault/logical.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/hashicorp/vault/api" 9 | ) 10 | 11 | func (v *Vault) List(path string) (*api.Secret, error) { 12 | return v.ListWithContext(context.Background(), path) 13 | } 14 | 15 | func (v *Vault) ListWithContext(ctx context.Context, path string) (*api.Secret, error) { 16 | // ctx, cancelFunc := c.c.withConfiguredTimeout(ctx) 17 | // defer cancelFunc() 18 | 19 | r := v.vault.NewRequest("LIST", "/v1/"+path) 20 | 21 | // Set this for broader compatibility, but we use LIST above to be able to 22 | // handle the wrapping lookup function 23 | r.Method = http.MethodGet 24 | r.Params.Set("list", "true") 25 | 26 | // resp, err := v.vault.RawRequestWithContext(ctx, r) 27 | resp, err := v.vault.Logical().ReadRawWithContext(ctx, path) 28 | if resp != nil { 29 | defer resp.Body.Close() 30 | } 31 | 32 | if resp != nil && resp.StatusCode == 404 { 33 | secret, parseErr := ParseSecret(resp.Body) 34 | switch parseErr { 35 | case nil: 36 | case io.EOF: 37 | return nil, nil 38 | default: 39 | return nil, parseErr 40 | } 41 | if secret != nil && (len(secret.Warnings) > 0 || len(secret.Data) > 0) { 42 | return secret, nil 43 | } 44 | return nil, nil 45 | } 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | return ParseSecret(resp.Body) 51 | } 52 | -------------------------------------------------------------------------------- /internal/vault/logical_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/vault" 7 | "github.com/dkyanakiev/vaulty/internal/vault/vaultfakes" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestList(t *testing.T) { 12 | path := "testpath" 13 | 14 | fakeLogical := &vaultfakes.FakeLogical{} 15 | 16 | v := &vault.Vault{ 17 | Logical: fakeLogical, 18 | } 19 | _, err := v.Logical.List(path) 20 | assert.NoError(t, err) 21 | 22 | } 23 | -------------------------------------------------------------------------------- /internal/vault/mounts.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/dkyanakiev/vaulty/internal/models" 9 | "github.com/hashicorp/vault/api" 10 | ) 11 | 12 | func (v *Vault) ListMounts() (map[string]*models.MountOutput, error) { 13 | 14 | apiMountList, err := v.vault.Sys().ListMounts() 15 | if err != nil { 16 | v.Logger.Warn().Err(err).Msg("Unable to access sys/mounts, attempting to use fallback method.\n") 17 | return v.listMountsFallback() 18 | } 19 | 20 | // Convert api.MountOutput to MountOutput 21 | mountList := make(map[string]*models.MountOutput) 22 | for k, v := range apiMountList { 23 | mountList[k] = toMount(v) 24 | } 25 | return mountList, nil 26 | 27 | } 28 | 29 | func (v *Vault) listMountsFallback() (map[string]*models.MountOutput, error) { 30 | 31 | resp, err := v.vault.Logical().ReadRaw("/sys/internal/ui/mounts") 32 | if err != nil { 33 | return nil, fmt.Errorf("failed to retrieve secret mounts: %w", err) 34 | } 35 | 36 | body, err := io.ReadAll(resp.Body) 37 | if err != nil { 38 | return nil, fmt.Errorf("failed to read response body: %w", err) 39 | } 40 | 41 | var response models.UiMountsResponse 42 | err = json.Unmarshal(body, &response) 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to unmarshal response: %w", err) 45 | } 46 | 47 | // Convert models.MountOutput to api.MountOutput 48 | mountList := make(map[string]*models.MountOutput) 49 | for k, v := range response.Data.Secret { 50 | mountList[k] = v 51 | } 52 | 53 | return mountList, nil 54 | } 55 | 56 | func (v *Vault) AllMounts() (map[string]*models.MountOutput, error) { 57 | 58 | mounts, err := v.ListMounts() 59 | if err != nil { 60 | return nil, fmt.Errorf("failed to retrieve mounts: %w", err) 61 | } 62 | return mounts, nil 63 | } 64 | 65 | func toMount(m *api.MountOutput) *models.MountOutput { 66 | 67 | return &models.MountOutput{ 68 | UUID: m.UUID, 69 | Type: m.Type, 70 | Description: m.Description, 71 | Accessor: m.Accessor, 72 | Config: models.MountConfigOutput{ 73 | //TODO:Fill out if needed 74 | DefaultLeaseTTL: m.Config.DefaultLeaseTTL, 75 | ListingVisibility: m.Config.ListingVisibility, 76 | }, 77 | Options: m.Options, 78 | Local: m.Local, 79 | SealWrap: m.SealWrap, 80 | ExternalEntropyAccess: m.ExternalEntropyAccess, 81 | PluginVersion: m.PluginVersion, 82 | RunningVersion: m.RunningVersion, 83 | RunningSha256: m.RunningSha256, 84 | DeprecationStatus: m.DeprecationStatus, 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /internal/vault/mounts_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/vault" 7 | "github.com/dkyanakiev/vaulty/internal/vault/vaultfakes" 8 | "github.com/hashicorp/vault/api" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestListMounts(t *testing.T) { 13 | 14 | fakeSys := &vaultfakes.FakeSys{} 15 | fakeSys.ListMountsReturns(map[string]*api.MountOutput{}, nil) 16 | 17 | v := &vault.Vault{ 18 | Sys: fakeSys, 19 | } 20 | 21 | _, err := v.Sys.ListMounts() 22 | assert.NoError(t, err) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /internal/vault/namespaces.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import "strings" 4 | 5 | // func (v *Vault) AllNamespaces() ([]string, error) { 6 | 7 | // } 8 | 9 | func (v *Vault) ChangeNamespace(ns string) []string { 10 | v.Logger.Debug().Msgf("Changing namespace to: %v", ns) 11 | v.vault.SetNamespace(ns) 12 | list, err := v.ListNamespaces() 13 | if err != nil { 14 | v.Logger.Err(err).Msgf("Failed to list namespaces: %v", err) 15 | } 16 | v.Logger.Debug().Msgf("New available namespaces are: %v", list) 17 | return list 18 | } 19 | 20 | func (v *Vault) SetNamespace(ns string) { 21 | v.Logger.Debug().Msgf("Changing namespace to: %v", ns) 22 | v.vault.SetNamespace(ns) 23 | } 24 | 25 | func (v *Vault) ListNamespaces() ([]string, error) { 26 | namespaces := []string{} 27 | secret, err := v.vault.Logical().List("sys/namespaces") 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | if secret != nil { 33 | keys, ok := secret.Data["keys"].([]interface{}) 34 | if !ok || len(keys) == 0 { 35 | return namespaces, nil 36 | } 37 | for _, namespace := range keys { 38 | trimmedNamespace := strings.TrimSuffix(namespace.(string), "/") 39 | namespaces = append(namespaces, trimmedNamespace) 40 | } 41 | } else { 42 | return namespaces, nil 43 | } 44 | return namespaces, nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/vault/policy.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | func (v *Vault) AllPolicies() ([]string, error) { 4 | pl, err := v.Sys.ListPolicies() 5 | if err != nil { 6 | return nil, err 7 | } 8 | 9 | policies := []string{} 10 | for _, p := range pl { 11 | policies = append(policies, p) 12 | } 13 | 14 | return policies, nil 15 | } 16 | 17 | func (v *Vault) GetPolicyInfo(name string) (string, error) { 18 | //TODO: Might need to make it a custom function 19 | policy, err := v.Sys.GetPolicy(name) 20 | if err != nil { 21 | return "", err 22 | } 23 | return policy, nil 24 | } 25 | -------------------------------------------------------------------------------- /internal/vault/policy_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/vault" 7 | "github.com/dkyanakiev/vaulty/internal/vault/vaultfakes" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestAllPolicies(t *testing.T) { 12 | 13 | fakeSys := &vaultfakes.FakeSys{} 14 | 15 | v := &vault.Vault{ 16 | Sys: fakeSys, 17 | } 18 | 19 | _, err := v.AllPolicies() 20 | 21 | assert.NoError(t, err) 22 | 23 | } 24 | 25 | func TestGetPolicyInfo(t *testing.T) { 26 | 27 | fakeSys := &vaultfakes.FakeSys{} 28 | 29 | v := &vault.Vault{ 30 | Sys: fakeSys, 31 | } 32 | 33 | _, err := v.GetPolicyInfo("test") 34 | 35 | assert.NoError(t, err) 36 | } 37 | -------------------------------------------------------------------------------- /internal/vault/vaultfakes/fake_client.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package vaultfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/vault" 8 | ) 9 | 10 | type FakeClient struct { 11 | AddressStub func() string 12 | addressMutex sync.RWMutex 13 | addressArgsForCall []struct { 14 | } 15 | addressReturns struct { 16 | result1 string 17 | } 18 | addressReturnsOnCall map[int]struct { 19 | result1 string 20 | } 21 | invocations map[string][][]interface{} 22 | invocationsMutex sync.RWMutex 23 | } 24 | 25 | func (fake *FakeClient) Address() string { 26 | fake.addressMutex.Lock() 27 | ret, specificReturn := fake.addressReturnsOnCall[len(fake.addressArgsForCall)] 28 | fake.addressArgsForCall = append(fake.addressArgsForCall, struct { 29 | }{}) 30 | stub := fake.AddressStub 31 | fakeReturns := fake.addressReturns 32 | fake.recordInvocation("Address", []interface{}{}) 33 | fake.addressMutex.Unlock() 34 | if stub != nil { 35 | return stub() 36 | } 37 | if specificReturn { 38 | return ret.result1 39 | } 40 | return fakeReturns.result1 41 | } 42 | 43 | func (fake *FakeClient) AddressCallCount() int { 44 | fake.addressMutex.RLock() 45 | defer fake.addressMutex.RUnlock() 46 | return len(fake.addressArgsForCall) 47 | } 48 | 49 | func (fake *FakeClient) AddressCalls(stub func() string) { 50 | fake.addressMutex.Lock() 51 | defer fake.addressMutex.Unlock() 52 | fake.AddressStub = stub 53 | } 54 | 55 | func (fake *FakeClient) AddressReturns(result1 string) { 56 | fake.addressMutex.Lock() 57 | defer fake.addressMutex.Unlock() 58 | fake.AddressStub = nil 59 | fake.addressReturns = struct { 60 | result1 string 61 | }{result1} 62 | } 63 | 64 | func (fake *FakeClient) AddressReturnsOnCall(i int, result1 string) { 65 | fake.addressMutex.Lock() 66 | defer fake.addressMutex.Unlock() 67 | fake.AddressStub = nil 68 | if fake.addressReturnsOnCall == nil { 69 | fake.addressReturnsOnCall = make(map[int]struct { 70 | result1 string 71 | }) 72 | } 73 | fake.addressReturnsOnCall[i] = struct { 74 | result1 string 75 | }{result1} 76 | } 77 | 78 | func (fake *FakeClient) Invocations() map[string][][]interface{} { 79 | fake.invocationsMutex.RLock() 80 | defer fake.invocationsMutex.RUnlock() 81 | fake.addressMutex.RLock() 82 | defer fake.addressMutex.RUnlock() 83 | copiedInvocations := map[string][][]interface{}{} 84 | for key, value := range fake.invocations { 85 | copiedInvocations[key] = value 86 | } 87 | return copiedInvocations 88 | } 89 | 90 | func (fake *FakeClient) recordInvocation(key string, args []interface{}) { 91 | fake.invocationsMutex.Lock() 92 | defer fake.invocationsMutex.Unlock() 93 | if fake.invocations == nil { 94 | fake.invocations = map[string][][]interface{}{} 95 | } 96 | if fake.invocations[key] == nil { 97 | fake.invocations[key] = [][]interface{}{} 98 | } 99 | fake.invocations[key] = append(fake.invocations[key], args) 100 | } 101 | 102 | var _ vault.Client = new(FakeClient) 103 | -------------------------------------------------------------------------------- /internal/vault/vaultfakes/fake_logical.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package vaultfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/vault" 8 | "github.com/hashicorp/vault/api" 9 | ) 10 | 11 | type FakeLogical struct { 12 | ListStub func(string) (*api.Secret, error) 13 | listMutex sync.RWMutex 14 | listArgsForCall []struct { 15 | arg1 string 16 | } 17 | listReturns struct { 18 | result1 *api.Secret 19 | result2 error 20 | } 21 | listReturnsOnCall map[int]struct { 22 | result1 *api.Secret 23 | result2 error 24 | } 25 | invocations map[string][][]interface{} 26 | invocationsMutex sync.RWMutex 27 | } 28 | 29 | func (fake *FakeLogical) List(arg1 string) (*api.Secret, error) { 30 | fake.listMutex.Lock() 31 | ret, specificReturn := fake.listReturnsOnCall[len(fake.listArgsForCall)] 32 | fake.listArgsForCall = append(fake.listArgsForCall, struct { 33 | arg1 string 34 | }{arg1}) 35 | stub := fake.ListStub 36 | fakeReturns := fake.listReturns 37 | fake.recordInvocation("List", []interface{}{arg1}) 38 | fake.listMutex.Unlock() 39 | if stub != nil { 40 | return stub(arg1) 41 | } 42 | if specificReturn { 43 | return ret.result1, ret.result2 44 | } 45 | return fakeReturns.result1, fakeReturns.result2 46 | } 47 | 48 | func (fake *FakeLogical) ListCallCount() int { 49 | fake.listMutex.RLock() 50 | defer fake.listMutex.RUnlock() 51 | return len(fake.listArgsForCall) 52 | } 53 | 54 | func (fake *FakeLogical) ListCalls(stub func(string) (*api.Secret, error)) { 55 | fake.listMutex.Lock() 56 | defer fake.listMutex.Unlock() 57 | fake.ListStub = stub 58 | } 59 | 60 | func (fake *FakeLogical) ListArgsForCall(i int) string { 61 | fake.listMutex.RLock() 62 | defer fake.listMutex.RUnlock() 63 | argsForCall := fake.listArgsForCall[i] 64 | return argsForCall.arg1 65 | } 66 | 67 | func (fake *FakeLogical) ListReturns(result1 *api.Secret, result2 error) { 68 | fake.listMutex.Lock() 69 | defer fake.listMutex.Unlock() 70 | fake.ListStub = nil 71 | fake.listReturns = struct { 72 | result1 *api.Secret 73 | result2 error 74 | }{result1, result2} 75 | } 76 | 77 | func (fake *FakeLogical) ListReturnsOnCall(i int, result1 *api.Secret, result2 error) { 78 | fake.listMutex.Lock() 79 | defer fake.listMutex.Unlock() 80 | fake.ListStub = nil 81 | if fake.listReturnsOnCall == nil { 82 | fake.listReturnsOnCall = make(map[int]struct { 83 | result1 *api.Secret 84 | result2 error 85 | }) 86 | } 87 | fake.listReturnsOnCall[i] = struct { 88 | result1 *api.Secret 89 | result2 error 90 | }{result1, result2} 91 | } 92 | 93 | func (fake *FakeLogical) Invocations() map[string][][]interface{} { 94 | fake.invocationsMutex.RLock() 95 | defer fake.invocationsMutex.RUnlock() 96 | fake.listMutex.RLock() 97 | defer fake.listMutex.RUnlock() 98 | copiedInvocations := map[string][][]interface{}{} 99 | for key, value := range fake.invocations { 100 | copiedInvocations[key] = value 101 | } 102 | return copiedInvocations 103 | } 104 | 105 | func (fake *FakeLogical) recordInvocation(key string, args []interface{}) { 106 | fake.invocationsMutex.Lock() 107 | defer fake.invocationsMutex.Unlock() 108 | if fake.invocations == nil { 109 | fake.invocations = map[string][][]interface{}{} 110 | } 111 | if fake.invocations[key] == nil { 112 | fake.invocations[key] = [][]interface{}{} 113 | } 114 | fake.invocations[key] = append(fake.invocations[key], args) 115 | } 116 | 117 | var _ vault.Logical = new(FakeLogical) 118 | -------------------------------------------------------------------------------- /internal/watcher/activity.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | type ActivityPool struct { 4 | Activities []chan struct{} 5 | } 6 | 7 | func (a *ActivityPool) Add(act chan struct{}) { 8 | a.Activities = append(a.Activities, act) 9 | } 10 | 11 | func (a *ActivityPool) DeactivateAll() { 12 | for a.hasActivities() { 13 | a.deactivate() 14 | } 15 | } 16 | 17 | func (a *ActivityPool) deactivate() { 18 | if a.hasActivities() { 19 | ch := a.Activities[0] 20 | ch <- struct{}{} 21 | a.Activities = a.Activities[1:] 22 | } 23 | } 24 | 25 | func (a *ActivityPool) hasActivities() bool { 26 | return len(a.Activities) > 0 27 | } 28 | -------------------------------------------------------------------------------- /internal/watcher/activity_test.go: -------------------------------------------------------------------------------- 1 | package watcher_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/watcher" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAdd(t *testing.T) { 11 | r := require.New(t) 12 | 13 | activity := &watcher.ActivityPool{} 14 | 15 | activity.Add(make(chan struct{})) 16 | r.Equal(len(activity.Activities), 1) 17 | 18 | activity.Add(make(chan struct{})) 19 | r.Equal(len(activity.Activities), 2) 20 | } 21 | 22 | func TestDeactivateAll(t *testing.T) { 23 | r := require.New(t) 24 | 25 | activity := &watcher.ActivityPool{} 26 | activity.Activities = []chan struct{}{ 27 | make(chan struct{}, 1), 28 | make(chan struct{}, 1), 29 | } 30 | 31 | activity.DeactivateAll() 32 | 33 | r.Empty(activity.Activities) 34 | } 35 | -------------------------------------------------------------------------------- /internal/watcher/mounts.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/models" 8 | ) 9 | 10 | func (w *Watcher) SubscribeToMounts(notify func()) { 11 | w.UpdateMounts() 12 | w.Subscribe(notify, "mounts") 13 | w.Notify("mounts") 14 | 15 | stop := make(chan struct{}) 16 | w.activities.Add(stop) 17 | ticker := time.NewTicker(30 * time.Second) 18 | go func() { 19 | for { 20 | select { 21 | case <-ticker.C: 22 | w.UpdateMounts() 23 | w.Notify("mounts") 24 | case <-stop: 25 | return 26 | } 27 | } 28 | }() 29 | } 30 | func (w *Watcher) UpdateMounts() { 31 | w.logger.Debug().Msg("Updating mounts") 32 | if w.state.Enterprise { 33 | w.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", w.state.SelectedNamespace) 34 | w.vault.SetNamespace(w.state.SelectedNamespace) 35 | } 36 | mounts, err := w.vault.AllMounts() 37 | if err != nil { 38 | log.Println(err) 39 | w.NotifyHandler(models.HandleError, err.Error()) 40 | } 41 | w.state.Mounts = mounts 42 | } 43 | -------------------------------------------------------------------------------- /internal/watcher/mounts_test.go: -------------------------------------------------------------------------------- 1 | package watcher_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/state" 8 | "github.com/dkyanakiev/vaulty/internal/watcher" 9 | "github.com/dkyanakiev/vaulty/internal/watcher/watcherfakes" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSubscribeToMounts(t *testing.T) { 14 | 15 | fakeVault := &watcherfakes.FakeVault{} 16 | state := state.New() 17 | fakeWatcher := watcher.NewWatcher(state, fakeVault, 2*time.Second, nil) 18 | 19 | notifyCalled := false 20 | notify := func() { 21 | notifyCalled = true 22 | } 23 | 24 | fakeWatcher.SubscribeToMounts(notify) 25 | 26 | assert.True(t, notifyCalled) 27 | } 28 | -------------------------------------------------------------------------------- /internal/watcher/namespaces.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/models" 8 | ) 9 | 10 | func (w *Watcher) SubscribeToNamespaces(notify func()) { 11 | w.UpdateNamespaces() 12 | w.Subscribe(notify, "namespaces") 13 | w.Notify("namespaces") 14 | 15 | stop := make(chan struct{}) 16 | w.activities.Add(stop) 17 | ticker := time.NewTicker(30 * time.Second) 18 | go func() { 19 | for { 20 | select { 21 | case <-ticker.C: 22 | w.UpdateNamespaces() 23 | w.Notify("namespaces") 24 | case <-stop: 25 | return 26 | } 27 | } 28 | }() 29 | } 30 | 31 | func (w *Watcher) UpdateNamespaces() { 32 | w.logger.Debug().Msg("Updating namespaces") 33 | if w.state.Enterprise { 34 | w.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", w.state.SelectedNamespace) 35 | w.vault.SetNamespace(w.state.SelectedNamespace) 36 | } 37 | namespaces, err := w.vault.ListNamespaces() 38 | if err != nil { 39 | log.Println(err) 40 | w.NotifyHandler(models.HandleError, err.Error()) 41 | } 42 | w.logger.Debug().Msgf("Namespaces: %v", namespaces) 43 | w.state.Namespaces = namespaces 44 | } 45 | -------------------------------------------------------------------------------- /internal/watcher/policies.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/models" 8 | ) 9 | 10 | func (w *Watcher) SubscribeToPolicies(notify func()) { 11 | 12 | w.updatePolicies() 13 | w.Subscribe(notify, "policies") 14 | w.Notify("policies") 15 | 16 | stop := make(chan struct{}) 17 | w.activities.Add(stop) 18 | ticker := time.NewTicker(w.interval) 19 | go func() { 20 | for { 21 | select { 22 | case <-ticker.C: 23 | w.updatePolicies() 24 | w.Notify("policies") 25 | case <-stop: 26 | return 27 | } 28 | } 29 | }() 30 | } 31 | 32 | func (w *Watcher) updatePolicies() { 33 | if w.state.Enterprise { 34 | w.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", w.state.SelectedNamespace) 35 | w.vault.SetNamespace(w.state.SelectedNamespace) 36 | } 37 | policies, err := w.vault.AllPolicies() 38 | if err != nil { 39 | w.NotifyHandler(models.HandleError, err.Error()) 40 | log.Println(err) 41 | } 42 | w.state.PolicyList = policies 43 | } 44 | -------------------------------------------------------------------------------- /internal/watcher/policyacl.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/models" 8 | ) 9 | 10 | func (w *Watcher) SubscribeToPoliciesACL(notify func()) { 11 | 12 | w.readPolicy() 13 | w.Subscribe(notify, "policyacl") 14 | w.Notify("policyacl") 15 | stop := make(chan struct{}) 16 | w.activities.Add(stop) 17 | ticker := time.NewTicker(w.interval) 18 | go func() { 19 | for { 20 | select { 21 | case <-ticker.C: 22 | w.readPolicy() 23 | w.Notify("policyacl") 24 | case <-stop: 25 | return 26 | } 27 | } 28 | }() 29 | } 30 | 31 | // func (w *Watcher) updatePoliciesAcl() { 32 | // policy, err := w.vault.GetPolicy(w.state.SelectedPolicyName) 33 | // if err != nil { 34 | // w.NotifyHandler(models.HandleError, err.Error()) 35 | // log.Println(err) 36 | // } 37 | // w.state.PolicyACL = policy 38 | // } 39 | 40 | func (w *Watcher) readPolicy() { 41 | if w.state.Enterprise { 42 | w.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", w.state.SelectedNamespace) 43 | w.vault.SetNamespace(w.state.SelectedNamespace) 44 | } 45 | policy, err := w.vault.GetPolicyInfo(w.state.SelectedPolicyName) 46 | if err != nil { 47 | w.NotifyHandler(models.HandleError, err.Error()) 48 | log.Println(err) 49 | } 50 | 51 | w.state.PolicyACL = policy 52 | } 53 | -------------------------------------------------------------------------------- /internal/watcher/secretobj.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | ) 8 | 9 | func (w *Watcher) SubscribeToSecret(selectedMount, selectedPath string, notify func()) { 10 | w.updateSecretState(selectedMount, selectedPath) 11 | w.Subscribe(notify, "secret") 12 | w.Notify("secret") 13 | 14 | stop := make(chan struct{}) 15 | w.activities.Add(stop) 16 | ticker := time.NewTicker(5 * time.Second) 17 | go func() { 18 | for { 19 | select { 20 | case <-ticker.C: 21 | w.updateSecretState(selectedMount, selectedPath) 22 | w.Notify("secret") 23 | case <-stop: 24 | return 25 | } 26 | } 27 | }() 28 | } 29 | 30 | func (w *Watcher) updateSecretState(selectedMount, selectedPath string) { 31 | if w.state.Enterprise { 32 | w.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", w.state.SelectedNamespace) 33 | w.vault.SetNamespace(w.state.SelectedNamespace) 34 | } 35 | secret, err := w.vault.GetSecretData(selectedMount, selectedPath) 36 | if err != nil { 37 | w.NotifyHandler(models.HandleInfo, err.Error()) 38 | } 39 | metadata, err2 := w.vault.GetSecretMetadata(selectedMount, selectedPath) 40 | if err2 != nil { 41 | w.NotifyHandler(models.HandleInfo, err2.Error()) 42 | } 43 | if err != nil && err2 != nil { 44 | w.NotifyHandler(models.HandleError, "Unable to return secret data or metadata") 45 | } 46 | w.state.SelectedSecret = secret 47 | w.state.SelectedSecretMeta = metadata 48 | 49 | } 50 | 51 | // TODO: Implement this 52 | func (w *Watcher) updateSecret(selectedMount, selectedPath string, update bool, data map[string]interface{}) { 53 | 54 | } 55 | -------------------------------------------------------------------------------- /internal/watcher/secrets.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | ) 8 | 9 | func (w *Watcher) SubscribeToSecrets(selectedMount, selectedPath string, notify func()) { 10 | w.updateSecrets(selectedMount, selectedPath) 11 | w.Subscribe(notify, "secrets") 12 | w.Notify("secrets") 13 | 14 | stop := make(chan struct{}) 15 | w.activities.Add(stop) 16 | ticker := time.NewTicker(w.interval) 17 | go func() { 18 | for { 19 | select { 20 | case <-ticker.C: 21 | w.updateSecrets(selectedMount, selectedPath) 22 | w.Notify("secrets") 23 | case <-stop: 24 | return 25 | } 26 | } 27 | }() 28 | } 29 | 30 | func (w *Watcher) updateSecrets(selectedMount, selectedPath string) { 31 | if w.state.Enterprise { 32 | w.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", w.state.SelectedNamespace) 33 | w.vault.SetNamespace(w.state.SelectedNamespace) 34 | } 35 | w.logger.Info().Msgf("Updating secrets for mount: %s, path: %s", selectedMount, selectedPath) 36 | secrets, err := w.vault.ListNestedSecrets(selectedMount, selectedPath) 37 | if err != nil { 38 | w.NotifyHandler(models.HandleError, err.Error()) 39 | 40 | } 41 | w.state.SecretsData = secrets 42 | 43 | } 44 | -------------------------------------------------------------------------------- /internal/watcher/watcher.go: -------------------------------------------------------------------------------- 1 | package watcher 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | "github.com/dkyanakiev/vaulty/internal/state" 8 | "github.com/hashicorp/vault/api" 9 | "github.com/rs/zerolog" 10 | ) 11 | 12 | //go:generate counterfeiter . Activities 13 | type Activities interface { 14 | Add(chan struct{}) 15 | DeactivateAll() 16 | } 17 | 18 | //go:generate counterfeiter . Vault 19 | type Vault interface { 20 | Address() string 21 | AllPolicies() ([]string, error) 22 | GetPolicyInfo(string) (string, error) 23 | AllMounts() (map[string]*models.MountOutput, error) 24 | ListSecrets(string) (*api.Secret, error) 25 | ListNestedSecrets(string, string) ([]models.SecretPath, error) 26 | SetNamespace(string) 27 | ListNamespaces() ([]string, error) 28 | GetSecretData(string, string) (*api.Secret, error) 29 | GetSecretMetadata(string, string) (*models.Metadata, error) 30 | //GetPolicy(string) (string, error) 31 | //ListPolicies() ([]string, error) 32 | } 33 | 34 | // Wather is used to track changes to the Vault instance and update the state. 35 | type Watcher struct { 36 | state *state.State 37 | handlers map[models.Handler]func(msg string, args ...interface{}) 38 | vault Vault 39 | logger *zerolog.Logger 40 | 41 | activities Activities 42 | interval time.Duration 43 | subscriber *subscriber 44 | //forceUpdate chan api.Topic 45 | } 46 | 47 | type subscriber struct { 48 | topics []string 49 | notify func() 50 | } 51 | 52 | func NewWatcher(state *state.State, vault Vault, interval time.Duration, logger *zerolog.Logger) *Watcher { 53 | return &Watcher{ 54 | state: state, 55 | vault: vault, 56 | handlers: map[models.Handler]func(ms string, args ...interface{}){}, 57 | interval: interval, 58 | logger: logger, 59 | activities: &ActivityPool{}, 60 | } 61 | } 62 | 63 | // Subscribe subscribes a function to a topic. This function should always be 64 | // called before Watcher.activities.Add(). 65 | func (w *Watcher) Subscribe(notify func(), topics ...string) { 66 | w.subscriber = &subscriber{ 67 | topics: topics, 68 | notify: notify, 69 | } 70 | 71 | // Whenever a subscription comes in make sure all running 72 | // goroutines (expect the main (Watch)) are stopped. 73 | w.activities.DeactivateAll() 74 | } 75 | 76 | // Unsubscribe removes the current subscriber. 77 | func (w *Watcher) Unsubscribe() { 78 | w.subscriber = nil 79 | w.activities.DeactivateAll() 80 | } 81 | 82 | // SubscribeHandler subscribes a handler to the watcher. This can be an for example an error 83 | // handler. The handler types are defined in the models package. 84 | func (w *Watcher) SubscribeHandler(handler models.Handler, handle func(string, ...interface{})) { 85 | w.handlers[handler] = handle 86 | } 87 | 88 | // NotifyHandler notifies a handler that an event occurred 89 | // on the topic it subscribed for. 90 | func (w *Watcher) NotifyHandler(handler models.Handler, msg string, args ...interface{}) { 91 | if _, ok := w.handlers[handler]; ok { 92 | w.handlers[handler](msg, args...) 93 | } 94 | } 95 | 96 | // Notify notifies the current subscriber on a specific topic (eg Jobs) 97 | // that data got updated in the state. 98 | func (w *Watcher) Notify(topic string) { 99 | if w.subscriber != nil && w.subscriber.notify != nil { 100 | for _, t := range w.subscriber.topics { 101 | if t == topic { 102 | w.subscriber.notify() 103 | } 104 | } 105 | } 106 | } 107 | 108 | func (w *Watcher) Watch() { 109 | //topic 110 | } 111 | -------------------------------------------------------------------------------- /internal/watcher/watcher_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package watcher_test 5 | 6 | import ( 7 | "io" 8 | "testing" 9 | "time" 10 | 11 | "github.com/dkyanakiev/vaulty/internal/models" 12 | "github.com/dkyanakiev/vaulty/internal/state" 13 | "github.com/dkyanakiev/vaulty/internal/watcher" 14 | "github.com/dkyanakiev/vaulty/internal/watcher/watcherfakes" 15 | "github.com/rs/zerolog" 16 | 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestSubscription(t *testing.T) { 21 | r := require.New(t) 22 | logger := zerolog.New(io.Discard) 23 | 24 | vault := &watcherfakes.FakeVault{} 25 | state := state.New() 26 | 27 | watcher := watcher.NewWatcher(state, vault, time.Second*2, &logger) 28 | 29 | var called bool 30 | fn := func() { 31 | called = true 32 | } 33 | 34 | watcher.Subscribe(fn, "policy") 35 | watcher.Notify("policy") 36 | 37 | r.True(called) 38 | 39 | called = false 40 | watcher.Unsubscribe() 41 | watcher.Notify("policy") 42 | 43 | r.False(called) 44 | } 45 | 46 | func TestHandlerSubscription(t *testing.T) { 47 | r := require.New(t) 48 | logger := zerolog.New(io.Discard) 49 | 50 | vault := &watcherfakes.FakeVault{} 51 | state := state.New() 52 | 53 | watcher := watcher.NewWatcher(state, vault, time.Second*2, &logger) 54 | 55 | var calledErrHandler bool 56 | handleErr := func(_ string, _ ...interface{}) { 57 | calledErrHandler = true 58 | } 59 | 60 | var calledInfoHandler bool 61 | handleInfo := func(_ string, _ ...interface{}) { 62 | calledInfoHandler = true 63 | } 64 | 65 | var calledFatalHandler bool 66 | handleFatal := func(_ string, _ ...interface{}) { 67 | calledFatalHandler = true 68 | } 69 | 70 | watcher.SubscribeHandler(models.HandleError, handleErr) 71 | watcher.SubscribeHandler(models.HandleInfo, handleInfo) 72 | watcher.SubscribeHandler(models.HandleFatal, handleFatal) 73 | 74 | watcher.NotifyHandler(models.HandleError, "error") 75 | watcher.NotifyHandler(models.HandleInfo, "info") 76 | watcher.NotifyHandler(models.HandleFatal, "fatal") 77 | 78 | r.True(calledErrHandler) 79 | r.True(calledInfoHandler) 80 | r.True(calledFatalHandler) 81 | } 82 | 83 | // TODO: Add more tests for the Watcher 84 | -------------------------------------------------------------------------------- /internal/watcher/watcherfakes/fake_activities.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package watcherfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/watcher" 8 | ) 9 | 10 | type FakeActivities struct { 11 | AddStub func(chan struct{}) 12 | addMutex sync.RWMutex 13 | addArgsForCall []struct { 14 | arg1 chan struct{} 15 | } 16 | DeactivateAllStub func() 17 | deactivateAllMutex sync.RWMutex 18 | deactivateAllArgsForCall []struct { 19 | } 20 | invocations map[string][][]interface{} 21 | invocationsMutex sync.RWMutex 22 | } 23 | 24 | func (fake *FakeActivities) Add(arg1 chan struct{}) { 25 | fake.addMutex.Lock() 26 | fake.addArgsForCall = append(fake.addArgsForCall, struct { 27 | arg1 chan struct{} 28 | }{arg1}) 29 | stub := fake.AddStub 30 | fake.recordInvocation("Add", []interface{}{arg1}) 31 | fake.addMutex.Unlock() 32 | if stub != nil { 33 | fake.AddStub(arg1) 34 | } 35 | } 36 | 37 | func (fake *FakeActivities) AddCallCount() int { 38 | fake.addMutex.RLock() 39 | defer fake.addMutex.RUnlock() 40 | return len(fake.addArgsForCall) 41 | } 42 | 43 | func (fake *FakeActivities) AddCalls(stub func(chan struct{})) { 44 | fake.addMutex.Lock() 45 | defer fake.addMutex.Unlock() 46 | fake.AddStub = stub 47 | } 48 | 49 | func (fake *FakeActivities) AddArgsForCall(i int) chan struct{} { 50 | fake.addMutex.RLock() 51 | defer fake.addMutex.RUnlock() 52 | argsForCall := fake.addArgsForCall[i] 53 | return argsForCall.arg1 54 | } 55 | 56 | func (fake *FakeActivities) DeactivateAll() { 57 | fake.deactivateAllMutex.Lock() 58 | fake.deactivateAllArgsForCall = append(fake.deactivateAllArgsForCall, struct { 59 | }{}) 60 | stub := fake.DeactivateAllStub 61 | fake.recordInvocation("DeactivateAll", []interface{}{}) 62 | fake.deactivateAllMutex.Unlock() 63 | if stub != nil { 64 | fake.DeactivateAllStub() 65 | } 66 | } 67 | 68 | func (fake *FakeActivities) DeactivateAllCallCount() int { 69 | fake.deactivateAllMutex.RLock() 70 | defer fake.deactivateAllMutex.RUnlock() 71 | return len(fake.deactivateAllArgsForCall) 72 | } 73 | 74 | func (fake *FakeActivities) DeactivateAllCalls(stub func()) { 75 | fake.deactivateAllMutex.Lock() 76 | defer fake.deactivateAllMutex.Unlock() 77 | fake.DeactivateAllStub = stub 78 | } 79 | 80 | func (fake *FakeActivities) Invocations() map[string][][]interface{} { 81 | fake.invocationsMutex.RLock() 82 | defer fake.invocationsMutex.RUnlock() 83 | fake.addMutex.RLock() 84 | defer fake.addMutex.RUnlock() 85 | fake.deactivateAllMutex.RLock() 86 | defer fake.deactivateAllMutex.RUnlock() 87 | copiedInvocations := map[string][][]interface{}{} 88 | for key, value := range fake.invocations { 89 | copiedInvocations[key] = value 90 | } 91 | return copiedInvocations 92 | } 93 | 94 | func (fake *FakeActivities) recordInvocation(key string, args []interface{}) { 95 | fake.invocationsMutex.Lock() 96 | defer fake.invocationsMutex.Unlock() 97 | if fake.invocations == nil { 98 | fake.invocations = map[string][][]interface{}{} 99 | } 100 | if fake.invocations[key] == nil { 101 | fake.invocations[key] = [][]interface{}{} 102 | } 103 | fake.invocations[key] = append(fake.invocations[key], args) 104 | } 105 | 106 | var _ watcher.Activities = new(FakeActivities) 107 | -------------------------------------------------------------------------------- /tui/component/commands.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/rivo/tview" 8 | 9 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 10 | "github.com/dkyanakiev/vaulty/tui/styles" 11 | ) 12 | 13 | var ( 14 | MainCommands = []string{ 15 | fmt.Sprintf("%sMain Commands:", styles.HighlightSecondaryTag), 16 | fmt.Sprintf("%sctrl-b%s to display Secret Engines", styles.HighlightPrimaryTag, styles.StandardColorTag), 17 | fmt.Sprintf("%sctrl-p%s to display ACL Policies", styles.HighlightPrimaryTag, styles.StandardColorTag), 18 | fmt.Sprintf("%sctrl-t%s to display Namespaces", styles.HighlightPrimaryTag, styles.StandardColorTag), 19 | fmt.Sprintf("%sctrl-c%s to Quit", styles.HighlightPrimaryTag, styles.StandardColorTag), 20 | } 21 | MountsCommands = []string{ 22 | fmt.Sprintf("\n%s Secret Engines Command List:", styles.HighlightSecondaryTag), 23 | fmt.Sprintf("%se or Enter%s to explore mount", styles.HighlightPrimaryTag, styles.StandardColorTag), 24 | } 25 | NoViewCommands = []string{} 26 | PolicyCommands = []string{ 27 | fmt.Sprintf("\n%s ACL Policy Commands:", styles.HighlightSecondaryTag), 28 | fmt.Sprintf("%si or %s to inspect policy", styles.HighlightPrimaryTag, styles.StandardColorTag), 29 | fmt.Sprintf("%s/%s Filter policies ", styles.HighlightPrimaryTag, styles.StandardColorTag), 30 | } 31 | PolicyACLCommands = []string{ 32 | fmt.Sprintf("\n%s ACL Policy Commands:", styles.HighlightSecondaryTag), 33 | fmt.Sprintf("%sesc%s to go back", styles.HighlightPrimaryTag, styles.StandardColorTag), 34 | // fmt.Sprintf("%s%s apply filter", styles.HighlightPrimaryTag, styles.StandardColorTag), 35 | } 36 | SecretsCommands = []string{ 37 | fmt.Sprintf("\n%s Secrets Commands:", styles.HighlightSecondaryTag), 38 | fmt.Sprintf("%se or enter%s to navigate to selected the path", styles.HighlightPrimaryTag, styles.StandardColorTag), 39 | fmt.Sprintf("%sb or esc%s to go back to the previous path", styles.HighlightPrimaryTag, styles.StandardColorTag), 40 | fmt.Sprintf("%sctrl-n%s to Create a new secret ", styles.HighlightPrimaryTag, styles.StandardColorTag), 41 | fmt.Sprintf("%s/%s Filter objects ", styles.HighlightPrimaryTag, styles.StandardColorTag), 42 | } 43 | SecretObjectCommands = []string{ 44 | fmt.Sprintf("\n%s Secret Commands:", styles.HighlightSecondaryTag), 45 | fmt.Sprintf("%sh%s toggle display for secrets", styles.HighlightPrimaryTag, styles.StandardColorTag), 46 | fmt.Sprintf("%st%s toggle display for metadata info", styles.HighlightPrimaryTag, styles.StandardColorTag), 47 | fmt.Sprintf("%sc%s copy secret to clipboard", styles.HighlightPrimaryTag, styles.StandardColorTag), 48 | fmt.Sprintf("%sj%s toggle json view for secret", styles.HighlightPrimaryTag, styles.StandardColorTag), 49 | fmt.Sprintf("%sP%s to PATCH secret", styles.HighlightPrimaryTag, styles.StandardColorTag), 50 | fmt.Sprintf("%sU%s to UPDATE secret", styles.HighlightPrimaryTag, styles.StandardColorTag), 51 | fmt.Sprintf("%sb or esc%s to go back to the previous path", styles.HighlightPrimaryTag, styles.StandardColorTag), 52 | } 53 | SecretsObjectPatchCommands = []string{ 54 | fmt.Sprintf("\n%s Secret Commands:", styles.HighlightSecondaryTag), 55 | fmt.Sprintf("%sctrl-w%s to submit your PATCH/UPDATE request", styles.HighlightPrimaryTag, styles.StandardColorTag), 56 | fmt.Sprintf("%sesc%s to go back to the previous path", styles.HighlightPrimaryTag, styles.StandardColorTag), 57 | } 58 | NamespaceObjectCommands = []string{ 59 | fmt.Sprintf("\n%s Namespace Commands:", styles.HighlightSecondaryTag), 60 | fmt.Sprintf("%sctrl-d%s to back to default namespace", styles.HighlightPrimaryTag, styles.StandardColorTag), 61 | fmt.Sprintf("%sctrl-w%s to back to root namespace", styles.HighlightPrimaryTag, styles.StandardColorTag), 62 | } 63 | ) 64 | 65 | type Commands struct { 66 | TextView TextView 67 | Props *CommandsProps 68 | slot *tview.Flex 69 | } 70 | 71 | type CommandsProps struct { 72 | MainCommands []string 73 | ViewCommands []string 74 | } 75 | 76 | func NewCommands() *Commands { 77 | return &Commands{ 78 | TextView: primitive.NewTextView(tview.AlignLeft), 79 | Props: &CommandsProps{ 80 | MainCommands: MainCommands, 81 | // ViewCommands: MainCommands, 82 | }, 83 | } 84 | } 85 | 86 | func (c *Commands) Update(commands []string) { 87 | c.Props.ViewCommands = commands 88 | c.updateText() 89 | } 90 | 91 | func (c *Commands) Render() error { 92 | if c.slot == nil { 93 | return ErrComponentNotBound 94 | } 95 | 96 | c.updateText() 97 | 98 | c.slot.AddItem(c.TextView.Primitive(), 0, 1, false) 99 | return nil 100 | } 101 | 102 | func (c *Commands) updateText() { 103 | commands := append(c.Props.MainCommands, c.Props.ViewCommands...) 104 | // Easy way to handle long list of commands for views 105 | if len(c.Props.ViewCommands) > 6 { 106 | commands = c.Props.ViewCommands 107 | } 108 | cmds := strings.Join(commands, "\n") 109 | c.TextView.SetText(cmds) 110 | } 111 | 112 | func (c *Commands) Bind(slot *tview.Flex) { 113 | c.slot = slot 114 | } 115 | -------------------------------------------------------------------------------- /tui/component/commands_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/rivo/tview" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestCommands_Update(t *testing.T) { 15 | commands := component.NewCommands() 16 | 17 | newCommands := []string{"new command 1", "new command 2"} 18 | commands.Update(newCommands) 19 | 20 | assert.Equal(t, newCommands, commands.Props.ViewCommands) 21 | } 22 | 23 | func TestCommands_OK(t *testing.T) { 24 | r := require.New(t) 25 | textView := &componentfakes.FakeTextView{} 26 | cmds := component.NewCommands() 27 | cmds.TextView = textView 28 | 29 | cmds.Props.MainCommands = []string{"command1", "command2"} 30 | cmds.Props.ViewCommands = []string{"subCmd1", "subCmd2"} 31 | 32 | cmds.Bind(tview.NewFlex()) 33 | err := cmds.Render() 34 | r.NoError(err) 35 | 36 | text := textView.SetTextArgsForCall(0) 37 | r.Equal(text, "command1\ncommand2\nsubCmd1\nsubCmd2") 38 | } 39 | 40 | func TestCommands_Fail(t *testing.T) { 41 | r := require.New(t) 42 | textView := &componentfakes.FakeTextView{} 43 | cmds := component.NewCommands() 44 | cmds.TextView = textView 45 | err := cmds.Render() 46 | r.Error(err) 47 | 48 | r.True(errors.Is(err, component.ErrComponentNotBound)) 49 | r.EqualError(err, "component not bound") 50 | } 51 | -------------------------------------------------------------------------------- /tui/component/componentfakes/fake_done_modal_func.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package componentfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | ) 9 | 10 | type FakeDoneModalFunc struct { 11 | Stub func(int, string) 12 | mutex sync.RWMutex 13 | argsForCall []struct { 14 | arg1 int 15 | arg2 string 16 | } 17 | invocations map[string][][]interface{} 18 | invocationsMutex sync.RWMutex 19 | } 20 | 21 | func (fake *FakeDoneModalFunc) Spy(arg1 int, arg2 string) { 22 | fake.mutex.Lock() 23 | fake.argsForCall = append(fake.argsForCall, struct { 24 | arg1 int 25 | arg2 string 26 | }{arg1, arg2}) 27 | stub := fake.Stub 28 | fake.recordInvocation("DoneModalFunc", []interface{}{arg1, arg2}) 29 | fake.mutex.Unlock() 30 | if stub != nil { 31 | fake.Stub(arg1, arg2) 32 | } 33 | } 34 | 35 | func (fake *FakeDoneModalFunc) CallCount() int { 36 | fake.mutex.RLock() 37 | defer fake.mutex.RUnlock() 38 | return len(fake.argsForCall) 39 | } 40 | 41 | func (fake *FakeDoneModalFunc) Calls(stub func(int, string)) { 42 | fake.mutex.Lock() 43 | defer fake.mutex.Unlock() 44 | fake.Stub = stub 45 | } 46 | 47 | func (fake *FakeDoneModalFunc) ArgsForCall(i int) (int, string) { 48 | fake.mutex.RLock() 49 | defer fake.mutex.RUnlock() 50 | return fake.argsForCall[i].arg1, fake.argsForCall[i].arg2 51 | } 52 | 53 | func (fake *FakeDoneModalFunc) Invocations() map[string][][]interface{} { 54 | fake.invocationsMutex.RLock() 55 | defer fake.invocationsMutex.RUnlock() 56 | fake.mutex.RLock() 57 | defer fake.mutex.RUnlock() 58 | copiedInvocations := map[string][][]interface{}{} 59 | for key, value := range fake.invocations { 60 | copiedInvocations[key] = value 61 | } 62 | return copiedInvocations 63 | } 64 | 65 | func (fake *FakeDoneModalFunc) recordInvocation(key string, args []interface{}) { 66 | fake.invocationsMutex.Lock() 67 | defer fake.invocationsMutex.Unlock() 68 | if fake.invocations == nil { 69 | fake.invocations = map[string][][]interface{}{} 70 | } 71 | if fake.invocations[key] == nil { 72 | fake.invocations[key] = [][]interface{}{} 73 | } 74 | fake.invocations[key] = append(fake.invocations[key], args) 75 | } 76 | 77 | var _ component.DoneModalFunc = new(FakeDoneModalFunc).Spy 78 | -------------------------------------------------------------------------------- /tui/component/componentfakes/fake_drop_down.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package componentfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | type FakeDropDown struct { 12 | PrimitiveStub func() tview.Primitive 13 | primitiveMutex sync.RWMutex 14 | primitiveArgsForCall []struct { 15 | } 16 | primitiveReturns struct { 17 | result1 tview.Primitive 18 | } 19 | primitiveReturnsOnCall map[int]struct { 20 | result1 tview.Primitive 21 | } 22 | SetCurrentOptionStub func(int) 23 | setCurrentOptionMutex sync.RWMutex 24 | setCurrentOptionArgsForCall []struct { 25 | arg1 int 26 | } 27 | SetOptionsStub func([]string, func(text string, index int)) 28 | setOptionsMutex sync.RWMutex 29 | setOptionsArgsForCall []struct { 30 | arg1 []string 31 | arg2 func(text string, index int) 32 | } 33 | SetSelectedFuncStub func(func(text string, index int)) 34 | setSelectedFuncMutex sync.RWMutex 35 | setSelectedFuncArgsForCall []struct { 36 | arg1 func(text string, index int) 37 | } 38 | invocations map[string][][]interface{} 39 | invocationsMutex sync.RWMutex 40 | } 41 | 42 | func (fake *FakeDropDown) Primitive() tview.Primitive { 43 | fake.primitiveMutex.Lock() 44 | ret, specificReturn := fake.primitiveReturnsOnCall[len(fake.primitiveArgsForCall)] 45 | fake.primitiveArgsForCall = append(fake.primitiveArgsForCall, struct { 46 | }{}) 47 | stub := fake.PrimitiveStub 48 | fakeReturns := fake.primitiveReturns 49 | fake.recordInvocation("Primitive", []interface{}{}) 50 | fake.primitiveMutex.Unlock() 51 | if stub != nil { 52 | return stub() 53 | } 54 | if specificReturn { 55 | return ret.result1 56 | } 57 | return fakeReturns.result1 58 | } 59 | 60 | func (fake *FakeDropDown) PrimitiveCallCount() int { 61 | fake.primitiveMutex.RLock() 62 | defer fake.primitiveMutex.RUnlock() 63 | return len(fake.primitiveArgsForCall) 64 | } 65 | 66 | func (fake *FakeDropDown) PrimitiveCalls(stub func() tview.Primitive) { 67 | fake.primitiveMutex.Lock() 68 | defer fake.primitiveMutex.Unlock() 69 | fake.PrimitiveStub = stub 70 | } 71 | 72 | func (fake *FakeDropDown) PrimitiveReturns(result1 tview.Primitive) { 73 | fake.primitiveMutex.Lock() 74 | defer fake.primitiveMutex.Unlock() 75 | fake.PrimitiveStub = nil 76 | fake.primitiveReturns = struct { 77 | result1 tview.Primitive 78 | }{result1} 79 | } 80 | 81 | func (fake *FakeDropDown) PrimitiveReturnsOnCall(i int, result1 tview.Primitive) { 82 | fake.primitiveMutex.Lock() 83 | defer fake.primitiveMutex.Unlock() 84 | fake.PrimitiveStub = nil 85 | if fake.primitiveReturnsOnCall == nil { 86 | fake.primitiveReturnsOnCall = make(map[int]struct { 87 | result1 tview.Primitive 88 | }) 89 | } 90 | fake.primitiveReturnsOnCall[i] = struct { 91 | result1 tview.Primitive 92 | }{result1} 93 | } 94 | 95 | func (fake *FakeDropDown) SetCurrentOption(arg1 int) { 96 | fake.setCurrentOptionMutex.Lock() 97 | fake.setCurrentOptionArgsForCall = append(fake.setCurrentOptionArgsForCall, struct { 98 | arg1 int 99 | }{arg1}) 100 | stub := fake.SetCurrentOptionStub 101 | fake.recordInvocation("SetCurrentOption", []interface{}{arg1}) 102 | fake.setCurrentOptionMutex.Unlock() 103 | if stub != nil { 104 | fake.SetCurrentOptionStub(arg1) 105 | } 106 | } 107 | 108 | func (fake *FakeDropDown) SetCurrentOptionCallCount() int { 109 | fake.setCurrentOptionMutex.RLock() 110 | defer fake.setCurrentOptionMutex.RUnlock() 111 | return len(fake.setCurrentOptionArgsForCall) 112 | } 113 | 114 | func (fake *FakeDropDown) SetCurrentOptionCalls(stub func(int)) { 115 | fake.setCurrentOptionMutex.Lock() 116 | defer fake.setCurrentOptionMutex.Unlock() 117 | fake.SetCurrentOptionStub = stub 118 | } 119 | 120 | func (fake *FakeDropDown) SetCurrentOptionArgsForCall(i int) int { 121 | fake.setCurrentOptionMutex.RLock() 122 | defer fake.setCurrentOptionMutex.RUnlock() 123 | argsForCall := fake.setCurrentOptionArgsForCall[i] 124 | return argsForCall.arg1 125 | } 126 | 127 | func (fake *FakeDropDown) SetOptions(arg1 []string, arg2 func(text string, index int)) { 128 | var arg1Copy []string 129 | if arg1 != nil { 130 | arg1Copy = make([]string, len(arg1)) 131 | copy(arg1Copy, arg1) 132 | } 133 | fake.setOptionsMutex.Lock() 134 | fake.setOptionsArgsForCall = append(fake.setOptionsArgsForCall, struct { 135 | arg1 []string 136 | arg2 func(text string, index int) 137 | }{arg1Copy, arg2}) 138 | stub := fake.SetOptionsStub 139 | fake.recordInvocation("SetOptions", []interface{}{arg1Copy, arg2}) 140 | fake.setOptionsMutex.Unlock() 141 | if stub != nil { 142 | fake.SetOptionsStub(arg1, arg2) 143 | } 144 | } 145 | 146 | func (fake *FakeDropDown) SetOptionsCallCount() int { 147 | fake.setOptionsMutex.RLock() 148 | defer fake.setOptionsMutex.RUnlock() 149 | return len(fake.setOptionsArgsForCall) 150 | } 151 | 152 | func (fake *FakeDropDown) SetOptionsCalls(stub func([]string, func(text string, index int))) { 153 | fake.setOptionsMutex.Lock() 154 | defer fake.setOptionsMutex.Unlock() 155 | fake.SetOptionsStub = stub 156 | } 157 | 158 | func (fake *FakeDropDown) SetOptionsArgsForCall(i int) ([]string, func(text string, index int)) { 159 | fake.setOptionsMutex.RLock() 160 | defer fake.setOptionsMutex.RUnlock() 161 | argsForCall := fake.setOptionsArgsForCall[i] 162 | return argsForCall.arg1, argsForCall.arg2 163 | } 164 | 165 | func (fake *FakeDropDown) SetSelectedFunc(arg1 func(text string, index int)) { 166 | fake.setSelectedFuncMutex.Lock() 167 | fake.setSelectedFuncArgsForCall = append(fake.setSelectedFuncArgsForCall, struct { 168 | arg1 func(text string, index int) 169 | }{arg1}) 170 | stub := fake.SetSelectedFuncStub 171 | fake.recordInvocation("SetSelectedFunc", []interface{}{arg1}) 172 | fake.setSelectedFuncMutex.Unlock() 173 | if stub != nil { 174 | fake.SetSelectedFuncStub(arg1) 175 | } 176 | } 177 | 178 | func (fake *FakeDropDown) SetSelectedFuncCallCount() int { 179 | fake.setSelectedFuncMutex.RLock() 180 | defer fake.setSelectedFuncMutex.RUnlock() 181 | return len(fake.setSelectedFuncArgsForCall) 182 | } 183 | 184 | func (fake *FakeDropDown) SetSelectedFuncCalls(stub func(func(text string, index int))) { 185 | fake.setSelectedFuncMutex.Lock() 186 | defer fake.setSelectedFuncMutex.Unlock() 187 | fake.SetSelectedFuncStub = stub 188 | } 189 | 190 | func (fake *FakeDropDown) SetSelectedFuncArgsForCall(i int) func(text string, index int) { 191 | fake.setSelectedFuncMutex.RLock() 192 | defer fake.setSelectedFuncMutex.RUnlock() 193 | argsForCall := fake.setSelectedFuncArgsForCall[i] 194 | return argsForCall.arg1 195 | } 196 | 197 | func (fake *FakeDropDown) Invocations() map[string][][]interface{} { 198 | fake.invocationsMutex.RLock() 199 | defer fake.invocationsMutex.RUnlock() 200 | fake.primitiveMutex.RLock() 201 | defer fake.primitiveMutex.RUnlock() 202 | fake.setCurrentOptionMutex.RLock() 203 | defer fake.setCurrentOptionMutex.RUnlock() 204 | fake.setOptionsMutex.RLock() 205 | defer fake.setOptionsMutex.RUnlock() 206 | fake.setSelectedFuncMutex.RLock() 207 | defer fake.setSelectedFuncMutex.RUnlock() 208 | copiedInvocations := map[string][][]interface{}{} 209 | for key, value := range fake.invocations { 210 | copiedInvocations[key] = value 211 | } 212 | return copiedInvocations 213 | } 214 | 215 | func (fake *FakeDropDown) recordInvocation(key string, args []interface{}) { 216 | fake.invocationsMutex.Lock() 217 | defer fake.invocationsMutex.Unlock() 218 | if fake.invocations == nil { 219 | fake.invocations = map[string][][]interface{}{} 220 | } 221 | if fake.invocations[key] == nil { 222 | fake.invocations[key] = [][]interface{}{} 223 | } 224 | fake.invocations[key] = append(fake.invocations[key], args) 225 | } 226 | 227 | var _ component.DropDown = new(FakeDropDown) 228 | -------------------------------------------------------------------------------- /tui/component/componentfakes/fake_modal.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package componentfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | type FakeModal struct { 12 | ContainerStub func() tview.Primitive 13 | containerMutex sync.RWMutex 14 | containerArgsForCall []struct { 15 | } 16 | containerReturns struct { 17 | result1 tview.Primitive 18 | } 19 | containerReturnsOnCall map[int]struct { 20 | result1 tview.Primitive 21 | } 22 | PrimitiveStub func() tview.Primitive 23 | primitiveMutex sync.RWMutex 24 | primitiveArgsForCall []struct { 25 | } 26 | primitiveReturns struct { 27 | result1 tview.Primitive 28 | } 29 | primitiveReturnsOnCall map[int]struct { 30 | result1 tview.Primitive 31 | } 32 | SetDoneFuncStub func(func(buttonIndex int, buttonLabel string)) 33 | setDoneFuncMutex sync.RWMutex 34 | setDoneFuncArgsForCall []struct { 35 | arg1 func(buttonIndex int, buttonLabel string) 36 | } 37 | SetFocusStub func(int) 38 | setFocusMutex sync.RWMutex 39 | setFocusArgsForCall []struct { 40 | arg1 int 41 | } 42 | SetTextStub func(string) 43 | setTextMutex sync.RWMutex 44 | setTextArgsForCall []struct { 45 | arg1 string 46 | } 47 | invocations map[string][][]interface{} 48 | invocationsMutex sync.RWMutex 49 | } 50 | 51 | func (fake *FakeModal) Container() tview.Primitive { 52 | fake.containerMutex.Lock() 53 | ret, specificReturn := fake.containerReturnsOnCall[len(fake.containerArgsForCall)] 54 | fake.containerArgsForCall = append(fake.containerArgsForCall, struct { 55 | }{}) 56 | stub := fake.ContainerStub 57 | fakeReturns := fake.containerReturns 58 | fake.recordInvocation("Container", []interface{}{}) 59 | fake.containerMutex.Unlock() 60 | if stub != nil { 61 | return stub() 62 | } 63 | if specificReturn { 64 | return ret.result1 65 | } 66 | return fakeReturns.result1 67 | } 68 | 69 | func (fake *FakeModal) ContainerCallCount() int { 70 | fake.containerMutex.RLock() 71 | defer fake.containerMutex.RUnlock() 72 | return len(fake.containerArgsForCall) 73 | } 74 | 75 | func (fake *FakeModal) ContainerCalls(stub func() tview.Primitive) { 76 | fake.containerMutex.Lock() 77 | defer fake.containerMutex.Unlock() 78 | fake.ContainerStub = stub 79 | } 80 | 81 | func (fake *FakeModal) ContainerReturns(result1 tview.Primitive) { 82 | fake.containerMutex.Lock() 83 | defer fake.containerMutex.Unlock() 84 | fake.ContainerStub = nil 85 | fake.containerReturns = struct { 86 | result1 tview.Primitive 87 | }{result1} 88 | } 89 | 90 | func (fake *FakeModal) ContainerReturnsOnCall(i int, result1 tview.Primitive) { 91 | fake.containerMutex.Lock() 92 | defer fake.containerMutex.Unlock() 93 | fake.ContainerStub = nil 94 | if fake.containerReturnsOnCall == nil { 95 | fake.containerReturnsOnCall = make(map[int]struct { 96 | result1 tview.Primitive 97 | }) 98 | } 99 | fake.containerReturnsOnCall[i] = struct { 100 | result1 tview.Primitive 101 | }{result1} 102 | } 103 | 104 | func (fake *FakeModal) Primitive() tview.Primitive { 105 | fake.primitiveMutex.Lock() 106 | ret, specificReturn := fake.primitiveReturnsOnCall[len(fake.primitiveArgsForCall)] 107 | fake.primitiveArgsForCall = append(fake.primitiveArgsForCall, struct { 108 | }{}) 109 | stub := fake.PrimitiveStub 110 | fakeReturns := fake.primitiveReturns 111 | fake.recordInvocation("Primitive", []interface{}{}) 112 | fake.primitiveMutex.Unlock() 113 | if stub != nil { 114 | return stub() 115 | } 116 | if specificReturn { 117 | return ret.result1 118 | } 119 | return fakeReturns.result1 120 | } 121 | 122 | func (fake *FakeModal) PrimitiveCallCount() int { 123 | fake.primitiveMutex.RLock() 124 | defer fake.primitiveMutex.RUnlock() 125 | return len(fake.primitiveArgsForCall) 126 | } 127 | 128 | func (fake *FakeModal) PrimitiveCalls(stub func() tview.Primitive) { 129 | fake.primitiveMutex.Lock() 130 | defer fake.primitiveMutex.Unlock() 131 | fake.PrimitiveStub = stub 132 | } 133 | 134 | func (fake *FakeModal) PrimitiveReturns(result1 tview.Primitive) { 135 | fake.primitiveMutex.Lock() 136 | defer fake.primitiveMutex.Unlock() 137 | fake.PrimitiveStub = nil 138 | fake.primitiveReturns = struct { 139 | result1 tview.Primitive 140 | }{result1} 141 | } 142 | 143 | func (fake *FakeModal) PrimitiveReturnsOnCall(i int, result1 tview.Primitive) { 144 | fake.primitiveMutex.Lock() 145 | defer fake.primitiveMutex.Unlock() 146 | fake.PrimitiveStub = nil 147 | if fake.primitiveReturnsOnCall == nil { 148 | fake.primitiveReturnsOnCall = make(map[int]struct { 149 | result1 tview.Primitive 150 | }) 151 | } 152 | fake.primitiveReturnsOnCall[i] = struct { 153 | result1 tview.Primitive 154 | }{result1} 155 | } 156 | 157 | func (fake *FakeModal) SetDoneFunc(arg1 func(buttonIndex int, buttonLabel string)) { 158 | fake.setDoneFuncMutex.Lock() 159 | fake.setDoneFuncArgsForCall = append(fake.setDoneFuncArgsForCall, struct { 160 | arg1 func(buttonIndex int, buttonLabel string) 161 | }{arg1}) 162 | stub := fake.SetDoneFuncStub 163 | fake.recordInvocation("SetDoneFunc", []interface{}{arg1}) 164 | fake.setDoneFuncMutex.Unlock() 165 | if stub != nil { 166 | fake.SetDoneFuncStub(arg1) 167 | } 168 | } 169 | 170 | func (fake *FakeModal) SetDoneFuncCallCount() int { 171 | fake.setDoneFuncMutex.RLock() 172 | defer fake.setDoneFuncMutex.RUnlock() 173 | return len(fake.setDoneFuncArgsForCall) 174 | } 175 | 176 | func (fake *FakeModal) SetDoneFuncCalls(stub func(func(buttonIndex int, buttonLabel string))) { 177 | fake.setDoneFuncMutex.Lock() 178 | defer fake.setDoneFuncMutex.Unlock() 179 | fake.SetDoneFuncStub = stub 180 | } 181 | 182 | func (fake *FakeModal) SetDoneFuncArgsForCall(i int) func(buttonIndex int, buttonLabel string) { 183 | fake.setDoneFuncMutex.RLock() 184 | defer fake.setDoneFuncMutex.RUnlock() 185 | argsForCall := fake.setDoneFuncArgsForCall[i] 186 | return argsForCall.arg1 187 | } 188 | 189 | func (fake *FakeModal) SetFocus(arg1 int) { 190 | fake.setFocusMutex.Lock() 191 | fake.setFocusArgsForCall = append(fake.setFocusArgsForCall, struct { 192 | arg1 int 193 | }{arg1}) 194 | stub := fake.SetFocusStub 195 | fake.recordInvocation("SetFocus", []interface{}{arg1}) 196 | fake.setFocusMutex.Unlock() 197 | if stub != nil { 198 | fake.SetFocusStub(arg1) 199 | } 200 | } 201 | 202 | func (fake *FakeModal) SetFocusCallCount() int { 203 | fake.setFocusMutex.RLock() 204 | defer fake.setFocusMutex.RUnlock() 205 | return len(fake.setFocusArgsForCall) 206 | } 207 | 208 | func (fake *FakeModal) SetFocusCalls(stub func(int)) { 209 | fake.setFocusMutex.Lock() 210 | defer fake.setFocusMutex.Unlock() 211 | fake.SetFocusStub = stub 212 | } 213 | 214 | func (fake *FakeModal) SetFocusArgsForCall(i int) int { 215 | fake.setFocusMutex.RLock() 216 | defer fake.setFocusMutex.RUnlock() 217 | argsForCall := fake.setFocusArgsForCall[i] 218 | return argsForCall.arg1 219 | } 220 | 221 | func (fake *FakeModal) SetText(arg1 string) { 222 | fake.setTextMutex.Lock() 223 | fake.setTextArgsForCall = append(fake.setTextArgsForCall, struct { 224 | arg1 string 225 | }{arg1}) 226 | stub := fake.SetTextStub 227 | fake.recordInvocation("SetText", []interface{}{arg1}) 228 | fake.setTextMutex.Unlock() 229 | if stub != nil { 230 | fake.SetTextStub(arg1) 231 | } 232 | } 233 | 234 | func (fake *FakeModal) SetTextCallCount() int { 235 | fake.setTextMutex.RLock() 236 | defer fake.setTextMutex.RUnlock() 237 | return len(fake.setTextArgsForCall) 238 | } 239 | 240 | func (fake *FakeModal) SetTextCalls(stub func(string)) { 241 | fake.setTextMutex.Lock() 242 | defer fake.setTextMutex.Unlock() 243 | fake.SetTextStub = stub 244 | } 245 | 246 | func (fake *FakeModal) SetTextArgsForCall(i int) string { 247 | fake.setTextMutex.RLock() 248 | defer fake.setTextMutex.RUnlock() 249 | argsForCall := fake.setTextArgsForCall[i] 250 | return argsForCall.arg1 251 | } 252 | 253 | func (fake *FakeModal) Invocations() map[string][][]interface{} { 254 | fake.invocationsMutex.RLock() 255 | defer fake.invocationsMutex.RUnlock() 256 | fake.containerMutex.RLock() 257 | defer fake.containerMutex.RUnlock() 258 | fake.primitiveMutex.RLock() 259 | defer fake.primitiveMutex.RUnlock() 260 | fake.setDoneFuncMutex.RLock() 261 | defer fake.setDoneFuncMutex.RUnlock() 262 | fake.setFocusMutex.RLock() 263 | defer fake.setFocusMutex.RUnlock() 264 | fake.setTextMutex.RLock() 265 | defer fake.setTextMutex.RUnlock() 266 | copiedInvocations := map[string][][]interface{}{} 267 | for key, value := range fake.invocations { 268 | copiedInvocations[key] = value 269 | } 270 | return copiedInvocations 271 | } 272 | 273 | func (fake *FakeModal) recordInvocation(key string, args []interface{}) { 274 | fake.invocationsMutex.Lock() 275 | defer fake.invocationsMutex.Unlock() 276 | if fake.invocations == nil { 277 | fake.invocations = map[string][][]interface{}{} 278 | } 279 | if fake.invocations[key] == nil { 280 | fake.invocations[key] = [][]interface{}{} 281 | } 282 | fake.invocations[key] = append(fake.invocations[key], args) 283 | } 284 | 285 | var _ component.Modal = new(FakeModal) 286 | -------------------------------------------------------------------------------- /tui/component/componentfakes/fake_selector.go: -------------------------------------------------------------------------------- 1 | // Code generated by counterfeiter. DO NOT EDIT. 2 | package componentfakes 3 | 4 | import ( 5 | "sync" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/primitives" 9 | "github.com/rivo/tview" 10 | ) 11 | 12 | type FakeSelector struct { 13 | ContainerStub func() tview.Primitive 14 | containerMutex sync.RWMutex 15 | containerArgsForCall []struct { 16 | } 17 | containerReturns struct { 18 | result1 tview.Primitive 19 | } 20 | containerReturnsOnCall map[int]struct { 21 | result1 tview.Primitive 22 | } 23 | GetTableStub func() *primitives.Table 24 | getTableMutex sync.RWMutex 25 | getTableArgsForCall []struct { 26 | } 27 | getTableReturns struct { 28 | result1 *primitives.Table 29 | } 30 | getTableReturnsOnCall map[int]struct { 31 | result1 *primitives.Table 32 | } 33 | PrimitiveStub func() tview.Primitive 34 | primitiveMutex sync.RWMutex 35 | primitiveArgsForCall []struct { 36 | } 37 | primitiveReturns struct { 38 | result1 tview.Primitive 39 | } 40 | primitiveReturnsOnCall map[int]struct { 41 | result1 tview.Primitive 42 | } 43 | invocations map[string][][]interface{} 44 | invocationsMutex sync.RWMutex 45 | } 46 | 47 | func (fake *FakeSelector) Container() tview.Primitive { 48 | fake.containerMutex.Lock() 49 | ret, specificReturn := fake.containerReturnsOnCall[len(fake.containerArgsForCall)] 50 | fake.containerArgsForCall = append(fake.containerArgsForCall, struct { 51 | }{}) 52 | stub := fake.ContainerStub 53 | fakeReturns := fake.containerReturns 54 | fake.recordInvocation("Container", []interface{}{}) 55 | fake.containerMutex.Unlock() 56 | if stub != nil { 57 | return stub() 58 | } 59 | if specificReturn { 60 | return ret.result1 61 | } 62 | return fakeReturns.result1 63 | } 64 | 65 | func (fake *FakeSelector) ContainerCallCount() int { 66 | fake.containerMutex.RLock() 67 | defer fake.containerMutex.RUnlock() 68 | return len(fake.containerArgsForCall) 69 | } 70 | 71 | func (fake *FakeSelector) ContainerCalls(stub func() tview.Primitive) { 72 | fake.containerMutex.Lock() 73 | defer fake.containerMutex.Unlock() 74 | fake.ContainerStub = stub 75 | } 76 | 77 | func (fake *FakeSelector) ContainerReturns(result1 tview.Primitive) { 78 | fake.containerMutex.Lock() 79 | defer fake.containerMutex.Unlock() 80 | fake.ContainerStub = nil 81 | fake.containerReturns = struct { 82 | result1 tview.Primitive 83 | }{result1} 84 | } 85 | 86 | func (fake *FakeSelector) ContainerReturnsOnCall(i int, result1 tview.Primitive) { 87 | fake.containerMutex.Lock() 88 | defer fake.containerMutex.Unlock() 89 | fake.ContainerStub = nil 90 | if fake.containerReturnsOnCall == nil { 91 | fake.containerReturnsOnCall = make(map[int]struct { 92 | result1 tview.Primitive 93 | }) 94 | } 95 | fake.containerReturnsOnCall[i] = struct { 96 | result1 tview.Primitive 97 | }{result1} 98 | } 99 | 100 | func (fake *FakeSelector) GetTable() *primitives.Table { 101 | fake.getTableMutex.Lock() 102 | ret, specificReturn := fake.getTableReturnsOnCall[len(fake.getTableArgsForCall)] 103 | fake.getTableArgsForCall = append(fake.getTableArgsForCall, struct { 104 | }{}) 105 | stub := fake.GetTableStub 106 | fakeReturns := fake.getTableReturns 107 | fake.recordInvocation("GetTable", []interface{}{}) 108 | fake.getTableMutex.Unlock() 109 | if stub != nil { 110 | return stub() 111 | } 112 | if specificReturn { 113 | return ret.result1 114 | } 115 | return fakeReturns.result1 116 | } 117 | 118 | func (fake *FakeSelector) GetTableCallCount() int { 119 | fake.getTableMutex.RLock() 120 | defer fake.getTableMutex.RUnlock() 121 | return len(fake.getTableArgsForCall) 122 | } 123 | 124 | func (fake *FakeSelector) GetTableCalls(stub func() *primitives.Table) { 125 | fake.getTableMutex.Lock() 126 | defer fake.getTableMutex.Unlock() 127 | fake.GetTableStub = stub 128 | } 129 | 130 | func (fake *FakeSelector) GetTableReturns(result1 *primitives.Table) { 131 | fake.getTableMutex.Lock() 132 | defer fake.getTableMutex.Unlock() 133 | fake.GetTableStub = nil 134 | fake.getTableReturns = struct { 135 | result1 *primitives.Table 136 | }{result1} 137 | } 138 | 139 | func (fake *FakeSelector) GetTableReturnsOnCall(i int, result1 *primitives.Table) { 140 | fake.getTableMutex.Lock() 141 | defer fake.getTableMutex.Unlock() 142 | fake.GetTableStub = nil 143 | if fake.getTableReturnsOnCall == nil { 144 | fake.getTableReturnsOnCall = make(map[int]struct { 145 | result1 *primitives.Table 146 | }) 147 | } 148 | fake.getTableReturnsOnCall[i] = struct { 149 | result1 *primitives.Table 150 | }{result1} 151 | } 152 | 153 | func (fake *FakeSelector) Primitive() tview.Primitive { 154 | fake.primitiveMutex.Lock() 155 | ret, specificReturn := fake.primitiveReturnsOnCall[len(fake.primitiveArgsForCall)] 156 | fake.primitiveArgsForCall = append(fake.primitiveArgsForCall, struct { 157 | }{}) 158 | stub := fake.PrimitiveStub 159 | fakeReturns := fake.primitiveReturns 160 | fake.recordInvocation("Primitive", []interface{}{}) 161 | fake.primitiveMutex.Unlock() 162 | if stub != nil { 163 | return stub() 164 | } 165 | if specificReturn { 166 | return ret.result1 167 | } 168 | return fakeReturns.result1 169 | } 170 | 171 | func (fake *FakeSelector) PrimitiveCallCount() int { 172 | fake.primitiveMutex.RLock() 173 | defer fake.primitiveMutex.RUnlock() 174 | return len(fake.primitiveArgsForCall) 175 | } 176 | 177 | func (fake *FakeSelector) PrimitiveCalls(stub func() tview.Primitive) { 178 | fake.primitiveMutex.Lock() 179 | defer fake.primitiveMutex.Unlock() 180 | fake.PrimitiveStub = stub 181 | } 182 | 183 | func (fake *FakeSelector) PrimitiveReturns(result1 tview.Primitive) { 184 | fake.primitiveMutex.Lock() 185 | defer fake.primitiveMutex.Unlock() 186 | fake.PrimitiveStub = nil 187 | fake.primitiveReturns = struct { 188 | result1 tview.Primitive 189 | }{result1} 190 | } 191 | 192 | func (fake *FakeSelector) PrimitiveReturnsOnCall(i int, result1 tview.Primitive) { 193 | fake.primitiveMutex.Lock() 194 | defer fake.primitiveMutex.Unlock() 195 | fake.PrimitiveStub = nil 196 | if fake.primitiveReturnsOnCall == nil { 197 | fake.primitiveReturnsOnCall = make(map[int]struct { 198 | result1 tview.Primitive 199 | }) 200 | } 201 | fake.primitiveReturnsOnCall[i] = struct { 202 | result1 tview.Primitive 203 | }{result1} 204 | } 205 | 206 | func (fake *FakeSelector) Invocations() map[string][][]interface{} { 207 | fake.invocationsMutex.RLock() 208 | defer fake.invocationsMutex.RUnlock() 209 | fake.containerMutex.RLock() 210 | defer fake.containerMutex.RUnlock() 211 | fake.getTableMutex.RLock() 212 | defer fake.getTableMutex.RUnlock() 213 | fake.primitiveMutex.RLock() 214 | defer fake.primitiveMutex.RUnlock() 215 | copiedInvocations := map[string][][]interface{}{} 216 | for key, value := range fake.invocations { 217 | copiedInvocations[key] = value 218 | } 219 | return copiedInvocations 220 | } 221 | 222 | func (fake *FakeSelector) recordInvocation(key string, args []interface{}) { 223 | fake.invocationsMutex.Lock() 224 | defer fake.invocationsMutex.Unlock() 225 | if fake.invocations == nil { 226 | fake.invocations = map[string][][]interface{}{} 227 | } 228 | if fake.invocations[key] == nil { 229 | fake.invocations[key] = [][]interface{}{} 230 | } 231 | fake.invocations[key] = append(fake.invocations[key], args) 232 | } 233 | 234 | var _ component.Selector = new(FakeSelector) 235 | -------------------------------------------------------------------------------- /tui/component/components.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/internal/models" 5 | "github.com/dkyanakiev/vaulty/tui/primitives" 6 | 7 | "github.com/gdamore/tcell/v2" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | const ( 12 | ErrComponentNotBound = models.Comp("component not bound") 13 | ErrComponentPropsNotSet = models.Comp("component properties not set") 14 | ) 15 | 16 | //go:generate counterfeiter . DoneModalFunc 17 | type DoneModalFunc func(buttonIndex int, buttonLabel string) 18 | 19 | type Primitive interface { 20 | Primitive() tview.Primitive 21 | } 22 | 23 | //go:generate counterfeiter . Table 24 | type Table interface { 25 | Primitive 26 | SetTitle(format string, args ...interface{}) 27 | GetCellContent(row, column int) string 28 | GetSelection() (row, column int) 29 | Clear() 30 | RenderHeader(data []string) 31 | RenderRow(data []string, index int, c tcell.Color) 32 | SetSelectedFunc(fn func(row, column int)) 33 | SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) 34 | ScrollToTop() *tview.Table 35 | SetSelectedStyle(style tcell.Style) 36 | SetSelectable(rows, columns bool) 37 | } 38 | 39 | //go:generate counterfeiter . TextView 40 | type TextView interface { 41 | Primitive 42 | GetText(bool) string 43 | SetBorder(bool) 44 | SetText(text string) *tview.TextView 45 | SetTitle(string) 46 | //Write(data []byte) (int, error) 47 | Highlight(regionIDs ...string) *tview.TextView 48 | Clear() *tview.TextView 49 | ModifyPrimitive(f func(t *tview.TextView)) 50 | ScrollToBeginning() *tview.TextView 51 | ScrollToEnd() *tview.TextView 52 | SetTextAlign(int) *tview.TextView 53 | } 54 | 55 | //go:generate counterfeiter . Modal 56 | type Modal interface { 57 | Primitive 58 | SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) 59 | SetText(text string) 60 | SetFocus(index int) 61 | Container() tview.Primitive 62 | } 63 | 64 | type Form interface { 65 | Primitive 66 | Container() tview.Primitive 67 | } 68 | 69 | //go:generate counterfeiter . InputField 70 | type InputField interface { 71 | Primitive 72 | SetDoneFunc(handler func(k tcell.Key)) 73 | SetChangedFunc(handler func(text string)) 74 | SetAutocompleteFunc(callback func(currentText string) (entries []string)) 75 | SetText(text string) 76 | GetText() string 77 | } 78 | 79 | //go:generate counterfeiter . DropDown 80 | type DropDown interface { 81 | Primitive 82 | SetOptions(options []string, selected func(text string, index int)) 83 | SetCurrentOption(index int) 84 | SetSelectedFunc(selected func(text string, index int)) 85 | } 86 | 87 | //go:generate counterfeiter . Selector 88 | type Selector interface { 89 | Primitive 90 | GetTable() *primitives.Table 91 | Container() tview.Primitive 92 | } 93 | 94 | //go:generate counterfeiter . TextArea 95 | type TextArea interface { 96 | Primitive 97 | SetText(string, bool) *tview.TextArea 98 | GetText() string 99 | SetBorder(bool) 100 | SetTitle(string) 101 | SetBorderColor(tcell.Color) 102 | } 103 | 104 | //go:generate counterfeiter . Box 105 | type Box interface { 106 | Primitive 107 | SetBorder(bool) *tview.Box 108 | SetTitle(string) *tview.Box 109 | SetBorderColor(tcell.Color) *tview.Box 110 | SetTitleColor(tcell.Color) *tview.Box 111 | } 112 | -------------------------------------------------------------------------------- /tui/component/constants.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | const ( 4 | LabelName = "Name" 5 | LabelNamespace = "Namespace" 6 | LabelDescription = "Description" 7 | MountID = "RowID" 8 | MountName = "Mount" 9 | MountPath = "Path" 10 | MountType = "Type" 11 | MountDescription = "Description" 12 | MountVersion = "Version" 13 | SecretPath = "Path" 14 | SecretObject = "Secret Object" 15 | ) 16 | -------------------------------------------------------------------------------- /tui/component/error.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | 7 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 8 | ) 9 | 10 | const PageNameError = "error" 11 | 12 | type Error struct { 13 | Modal Modal 14 | Props *ErrorProps 15 | pages *tview.Pages 16 | } 17 | 18 | type ErrorProps struct { 19 | Done DoneModalFunc 20 | } 21 | 22 | func NewError() *Error { 23 | buttons := []string{"OK"} 24 | modal := primitive.NewModal("Error", buttons, tcell.ColorDarkRed) 25 | 26 | return &Error{ 27 | Modal: modal, 28 | Props: &ErrorProps{}, 29 | } 30 | } 31 | 32 | func (e *Error) Render(msg string) error { 33 | if e.Props.Done == nil { 34 | return ErrComponentPropsNotSet 35 | } 36 | 37 | if e.pages == nil { 38 | return ErrComponentNotBound 39 | } 40 | e.Modal.SetDoneFunc(e.Props.Done) 41 | e.Modal.SetText(msg) 42 | e.pages.AddPage(PageNameError, e.Modal.Container(), true, true) 43 | 44 | return nil 45 | } 46 | 47 | func (e *Error) Bind(pages *tview.Pages) { 48 | e.pages = pages 49 | } 50 | -------------------------------------------------------------------------------- /tui/component/error_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/rivo/tview" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestError_Ok(t *testing.T) { 14 | r := require.New(t) 15 | 16 | e := component.NewError() 17 | 18 | modal := &componentfakes.FakeModal{} 19 | e.Modal = modal 20 | 21 | var doneCalled bool 22 | e.Props.Done = func(buttonIndex int, buttonLabel string) { 23 | doneCalled = true 24 | } 25 | 26 | pages := tview.NewPages() 27 | e.Bind(pages) 28 | 29 | err := e.Render("error") 30 | r.NoError(err) 31 | 32 | actualDone := modal.SetDoneFuncArgsForCall(0) 33 | text := modal.SetTextArgsForCall(0) 34 | 35 | actualDone(0, "buttonName") 36 | 37 | r.True(doneCalled) 38 | r.Equal(text, "error") 39 | } 40 | 41 | func TestError_Fail(t *testing.T) { 42 | r := require.New(t) 43 | 44 | t.Run("When the component isn't bound", func(t *testing.T) { 45 | e := component.NewError() 46 | 47 | modal := &componentfakes.FakeModal{} 48 | e.Modal = modal 49 | 50 | e.Props.Done = func(buttonIndex int, buttonLabel string) {} 51 | 52 | err := e.Render("error") 53 | r.Error(err) 54 | 55 | // It provides the correct error message 56 | r.EqualError(err, "component not bound") 57 | 58 | // It is the correct error 59 | r.True(errors.Is(err, component.ErrComponentNotBound)) 60 | }) 61 | 62 | t.Run("When DoneFunc is not set", func(t *testing.T) { 63 | e := component.NewError() 64 | 65 | modal := &componentfakes.FakeModal{} 66 | e.Modal = modal 67 | 68 | pages := tview.NewPages() 69 | e.Bind(pages) 70 | 71 | err := e.Render("error") 72 | r.Error(err) 73 | 74 | // It provides the correct error message 75 | r.EqualError(err, "component properties not set") 76 | 77 | // It is the correct error 78 | r.True(errors.Is(err, component.ErrComponentPropsNotSet)) 79 | }) 80 | } 81 | -------------------------------------------------------------------------------- /tui/component/info.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | 6 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 7 | "github.com/dkyanakiev/vaulty/tui/styles" 8 | ) 9 | 10 | const PageNameInfo = "info" 11 | 12 | type Info struct { 13 | Modal Modal 14 | Props *InfoProps 15 | pages *tview.Pages 16 | } 17 | 18 | type InfoProps struct { 19 | Done DoneModalFunc 20 | } 21 | 22 | func NewInfo() *Info { 23 | buttons := []string{"OK"} 24 | modal := primitive.NewModal("Info", buttons, styles.TcellColorModalInfo) 25 | 26 | return &Info{ 27 | Modal: modal, 28 | Props: &InfoProps{}, 29 | } 30 | } 31 | 32 | func (i *Info) Render(msg string) error { 33 | if i.Props.Done == nil { 34 | return ErrComponentPropsNotSet 35 | } 36 | 37 | if i.pages == nil { 38 | return ErrComponentNotBound 39 | } 40 | 41 | i.Modal.SetDoneFunc(i.Props.Done) 42 | i.Modal.SetText(msg) 43 | i.pages.AddPage(PageNameInfo, i.Modal.Container(), true, true) 44 | 45 | return nil 46 | } 47 | 48 | func (i *Info) Bind(pages *tview.Pages) { 49 | i.pages = pages 50 | } 51 | -------------------------------------------------------------------------------- /tui/component/info_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/rivo/tview" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestInfo_Pass(t *testing.T) { 14 | r := require.New(t) 15 | 16 | e := component.NewInfo() 17 | 18 | modal := &componentfakes.FakeModal{} 19 | e.Modal = modal 20 | pages := tview.NewPages() 21 | e.Bind(pages) 22 | 23 | e.Props.Done = func(buttonIndex int, buttonLabel string) { 24 | 25 | } 26 | 27 | err := e.Render("Info") 28 | r.NoError(err) 29 | 30 | // actualDone := modal.SetDoneFuncArgsForCall(0) 31 | text := modal.SetTextArgsForCall(0) 32 | 33 | // actualDone(0, "OK") 34 | 35 | r.Equal(text, "Info") 36 | } 37 | 38 | func TestInfo_Fail(t *testing.T) { 39 | r := require.New(t) 40 | 41 | t.Run("When the component isn't bound", func(t *testing.T) { 42 | e := component.NewInfo() 43 | 44 | e.Props.Done = func(buttonIndex int, buttonLabel string) {} 45 | 46 | err := e.Render("Info") 47 | r.Error(err) 48 | 49 | // It provides the correct error message 50 | r.EqualError(err, "component not bound") 51 | 52 | // It is the correct error 53 | r.True(errors.Is(err, component.ErrComponentNotBound)) 54 | }) 55 | 56 | t.Run("When DoneFunc is not set", func(t *testing.T) { 57 | e := component.NewInfo() 58 | 59 | pages := tview.NewPages() 60 | e.Bind(pages) 61 | 62 | err := e.Render("error") 63 | r.Error(err) 64 | 65 | // It provides the correct error message 66 | r.EqualError(err, "component properties not set") 67 | 68 | // It is the correct error 69 | r.True(errors.Is(err, component.ErrComponentPropsNotSet)) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /tui/component/jump.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/primitives" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | // Not currently used. Might need modification to reuse this for both paths and policies. 10 | 11 | const jumpToPlaceholder = "(hit enter or esc to leave)" 12 | 13 | type SetDoneFunc func(key tcell.Key) 14 | 15 | type JumpToPolicy struct { 16 | InputField InputField 17 | Props *JumpToPolicyProps 18 | slot *tview.Flex 19 | } 20 | 21 | type JumpToPolicyProps struct { 22 | DoneFunc SetDoneFunc 23 | } 24 | 25 | func NewJumpToPolicy() *JumpToPolicy { 26 | jj := &JumpToPolicy{} 27 | jj.Props = &JumpToPolicyProps{} 28 | 29 | in := primitives.NewInputField("jump: ", jumpToPlaceholder) 30 | 31 | in.SetAutocompleteFunc(func(currentText string) (entries []string) { 32 | return jj.find(currentText) 33 | }) 34 | 35 | jj.InputField = in 36 | return jj 37 | } 38 | 39 | func (jj *JumpToPolicy) Render() error { 40 | if err := jj.validate(); err != nil { 41 | return err 42 | } 43 | 44 | jj.InputField.SetDoneFunc(jj.Props.DoneFunc) 45 | jj.slot.AddItem(jj.InputField.Primitive(), 0, 2, false) 46 | return nil 47 | } 48 | 49 | func (jj *JumpToPolicy) validate() error { 50 | if jj.Props.DoneFunc == nil { 51 | return ErrComponentPropsNotSet 52 | } 53 | 54 | if jj.slot == nil { 55 | return ErrComponentNotBound 56 | } 57 | 58 | return nil 59 | } 60 | 61 | func (jj *JumpToPolicy) Bind(slot *tview.Flex) { 62 | jj.slot = slot 63 | } 64 | 65 | func (jj *JumpToPolicy) find(text string) []string { 66 | result := []string{} 67 | if text == "" { 68 | return result 69 | } 70 | 71 | // for _, j := range jj.Props.Jobs { 72 | // ok := strings.Contains(j.ID, text) 73 | // if ok { 74 | // result = append(result, j.ID) 75 | // } 76 | // } 77 | 78 | return result 79 | } 80 | -------------------------------------------------------------------------------- /tui/component/logo.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | var LogoASCII = []string{ 12 | `[#00b57c] `, 13 | `____ ____ .___________ `, 14 | `\ \ / /____ __ __| \______ \___.__.`, 15 | ` \ Y /\__ \ | | \ | / < | |`, 16 | ` \ / / __ \| | / |__/ / \___ |`, 17 | ` \___/ (____ /____/|____/____/ / ____|`, 18 | ` \/ \/ `, 19 | `[#26ffe6]Vaul7y - Terminal Dashboard`, 20 | } 21 | 22 | type Logo struct { 23 | TextView TextView 24 | slot *tview.Flex 25 | Props *LogoProps 26 | } 27 | 28 | type LogoProps struct { 29 | Version string 30 | } 31 | 32 | func NewLogo(version string) *Logo { 33 | t := primitive.NewTextView(tview.AlignRight) 34 | return &Logo{ 35 | TextView: t, 36 | Props: &LogoProps{ 37 | Version: version, 38 | }, 39 | } 40 | } 41 | 42 | func (l *Logo) Render() error { 43 | if l.slot == nil { 44 | return ErrComponentNotBound 45 | } 46 | 47 | versionText := fmt.Sprintf("[#26ffe6]version: %s", l.Props.Version) 48 | logo := strings.Join(LogoASCII, "\n") 49 | logo = fmt.Sprintf("%s\n%s", logo, versionText) 50 | l.TextView.SetText(logo) 51 | l.slot.AddItem(l.TextView.Primitive(), 0, 1, false) 52 | return nil 53 | } 54 | 55 | func (l *Logo) Bind(slot *tview.Flex) { 56 | l.slot = slot 57 | } 58 | -------------------------------------------------------------------------------- /tui/component/logo_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/dkyanakiev/vaulty/tui/component" 10 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 11 | "github.com/rivo/tview" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestLogo_Pass(t *testing.T) { 16 | r := require.New(t) 17 | 18 | textView := &componentfakes.FakeTextView{} 19 | logo := component.NewLogo("0.0.0") 20 | logo.TextView = textView 21 | 22 | logo.Bind(tview.NewFlex()) 23 | 24 | err := logo.Render() 25 | r.NoError(err) 26 | 27 | text := textView.SetTextArgsForCall(0) 28 | versionText := fmt.Sprintf("[#26ffe6]version: %s", "0.0.0") 29 | expectedLogo := strings.Join(component.LogoASCII, "\n") 30 | expectedLogo = fmt.Sprintf("%s\n%s", expectedLogo, versionText) 31 | r.Equal(text, expectedLogo) 32 | } 33 | 34 | func TestLogo_Fail(t *testing.T) { 35 | r := require.New(t) 36 | logo := component.NewLogo("0.0.0") 37 | 38 | err := logo.Render() 39 | r.Error(err) 40 | 41 | r.True(errors.Is(err, component.ErrComponentNotBound)) 42 | r.EqualError(err, "component not bound") 43 | } 44 | -------------------------------------------------------------------------------- /tui/component/mounts_table.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "time" 7 | 8 | "github.com/dkyanakiev/vaulty/internal/models" 9 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 10 | "github.com/dkyanakiev/vaulty/tui/styles" 11 | "github.com/gdamore/tcell/v2" 12 | "github.com/rivo/tview" 13 | "github.com/rs/zerolog" 14 | ) 15 | 16 | const ( 17 | TableTitleMounts = "Secret Engines" 18 | ) 19 | 20 | var ( 21 | TableHeaderJobs = []string{ 22 | MountPath, 23 | MountName, 24 | MountDescription, 25 | MountType, 26 | } 27 | ) 28 | 29 | type SelectMountPathFunc func(mountPath string) 30 | 31 | type MountsTable struct { 32 | Table Table 33 | Props *MountsTableProps 34 | Logger *zerolog.Logger 35 | 36 | slot *tview.Flex 37 | } 38 | 39 | type MountsTableProps struct { 40 | SelectedMount string 41 | SelectPath SelectMountPathFunc 42 | HandleNoResources models.HandlerFunc 43 | // Data map[string]*models.MountOutput 44 | 45 | Data map[string]*models.MountOutput 46 | Namespace string 47 | } 48 | 49 | func NewMountsTable() *MountsTable { 50 | t := primitive.NewTable() 51 | 52 | jt := &MountsTable{ 53 | Table: t, 54 | Props: &MountsTableProps{}, 55 | } 56 | 57 | return jt 58 | } 59 | 60 | func (m *MountsTable) Bind(slot *tview.Flex) { 61 | m.slot = slot 62 | } 63 | 64 | func (m *MountsTable) Render() error { 65 | 66 | m.reset() 67 | m.Table.SetTitle("%s (%s)", TableTitleMounts, "Default") 68 | 69 | if len(m.Props.Data) == 0 { 70 | m.Props.HandleNoResources( 71 | "%sno mounts available\n¯%s\\_( ͡• ͜ʖ ͡•)_/¯", 72 | styles.HighlightPrimaryTag, 73 | styles.HighlightSecondaryTag, 74 | ) 75 | 76 | return nil 77 | } 78 | 79 | m.Table.SetSelectedFunc(m.mountSelected) 80 | m.Table.RenderHeader(TableHeaderJobs) 81 | m.renderRows() 82 | 83 | m.slot.AddItem(m.Table.Primitive(), 0, 1, false) 84 | return nil 85 | } 86 | 87 | func (m *MountsTable) GetIDForSelection() string { 88 | row, _ := m.Table.GetSelection() 89 | return m.Table.GetCellContent(row, 0) 90 | } 91 | 92 | func (m *MountsTable) validate() error { 93 | // TODO: Revisid validation 94 | if m.Props.SelectedMount == "" || m.Props.HandleNoResources == nil { 95 | m.Logger.Err(ErrComponentPropsNotSet).Msgf("Random error: %s", ErrComponentPropsNotSet) 96 | } 97 | 98 | if m.slot == nil { 99 | return ErrComponentNotBound 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (m *MountsTable) reset() { 106 | m.slot.Clear() 107 | m.Table.Clear() 108 | } 109 | 110 | func (m *MountsTable) mountSelected(row, _ int) { 111 | mountPath := m.Table.GetCellContent(row, 0) 112 | m.Props.SelectedMount = mountPath 113 | } 114 | 115 | func (m *MountsTable) renderRows() { 116 | index := 0 117 | 118 | keys := make([]string, 0, len(m.Props.Data)) 119 | for k := range m.Props.Data { 120 | keys = append(keys, k) 121 | } 122 | sort.Strings(keys) 123 | for _, k := range keys { 124 | mount := m.Props.Data[k] 125 | if mount.Type == "kv" { 126 | row := []string{ 127 | k, 128 | mount.Type, 129 | mount.Description, 130 | mount.RunningVersion, 131 | } 132 | index = index + 1 133 | 134 | c := m.cellColor(mount.Type) 135 | 136 | m.Table.RenderRow(row, index, c) 137 | } 138 | } 139 | 140 | } 141 | 142 | func (m *MountsTable) cellColor(mountType string) tcell.Color { 143 | c := tcell.ColorWhite 144 | // Setup splits based on type 145 | switch mountType { 146 | case models.MountTypeSystem: 147 | c = styles.TcellColorAttention 148 | case models.MountTypeCubbyhole: 149 | c = tcell.ColorYellow 150 | case models.MountTypeIdentity: 151 | c = tcell.ColorRed 152 | case models.MountTypeKV: 153 | c = tcell.ColorGreenYellow 154 | case models.MountTypePki: 155 | c = tcell.ColorBlue 156 | } 157 | 158 | return c 159 | } 160 | 161 | func formatTimeSince(since time.Duration) string { 162 | if since.Seconds() < 60 { 163 | return fmt.Sprintf("%.0fs", since.Seconds()) 164 | } 165 | 166 | if since.Minutes() < 60 { 167 | return fmt.Sprintf("%.0fm", since.Minutes()) 168 | } 169 | 170 | if since.Hours() < 60 { 171 | return fmt.Sprintf("%.0fh", since.Hours()) 172 | } 173 | 174 | return fmt.Sprintf("%.0fd", (since.Hours() / 24)) 175 | } 176 | -------------------------------------------------------------------------------- /tui/component/mounts_table_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/dkyanakiev/vaulty/tui/styles" 10 | "github.com/gdamore/tcell/v2" 11 | "github.com/rivo/tview" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestMountsTable_Pass(t *testing.T) { 16 | r := require.New(t) 17 | t.Run("When there is data to render", func(t *testing.T) { 18 | 19 | fakeTable := &componentfakes.FakeTable{} 20 | mTable := component.NewMountsTable() 21 | 22 | mTable.Table = fakeTable 23 | mTable.Props.Namespace = "default" 24 | mTable.Props.Data = map[string]*models.MountOutput{ 25 | "path-one/": { 26 | Type: "kv", 27 | Description: "description", 28 | RunningVersion: "v0.15", 29 | }, 30 | } 31 | 32 | mTable.Props.SelectPath = func(id string) {} 33 | mTable.Props.HandleNoResources = func(format string, args ...interface{}) {} 34 | slot := tview.NewFlex() 35 | mTable.Bind(slot) 36 | // It doesn't error 37 | err := mTable.Render() 38 | r.NoError(err) 39 | 40 | // Render header rows 41 | renderHeaderCount := fakeTable.RenderHeaderCallCount() 42 | r.Equal(renderHeaderCount, 1) 43 | 44 | // Correct headers 45 | header := fakeTable.RenderHeaderArgsForCall(0) 46 | r.Equal(component.TableHeaderJobs, header) 47 | 48 | // It renders the correct number of rows 49 | renderRowCallCount := fakeTable.RenderRowCallCount() 50 | r.Equal(renderRowCallCount, 1) 51 | 52 | row1, index1, c1 := fakeTable.RenderRowArgsForCall(0) 53 | expectedRow1 := []string{"path-one/", "kv", "description", "v0.15"} 54 | 55 | r.Equal(expectedRow1, row1) 56 | r.Equal(index1, 1) 57 | r.Equal(c1, tcell.ColorGreenYellow) 58 | 59 | }) 60 | 61 | t.Run("No data to render", func(t *testing.T) { 62 | fakeTable := &componentfakes.FakeTable{} 63 | mTable := component.NewMountsTable() 64 | 65 | mTable.Table = fakeTable 66 | mTable.Props.Namespace = "default" 67 | mTable.Props.Data = map[string]*models.MountOutput{} 68 | 69 | var NoResourcesCalled bool 70 | mTable.Props.HandleNoResources = func(format string, args ...interface{}) { 71 | NoResourcesCalled = true 72 | 73 | r.Equal("%sno mounts available\n¯%s\\_( ͡• ͜ʖ ͡•)_/¯", format) 74 | r.Len(args, 2) 75 | r.Equal(args[0], styles.HighlightPrimaryTag) 76 | r.Equal(args[1], styles.HighlightSecondaryTag) 77 | } 78 | 79 | slot := tview.NewFlex() 80 | mTable.Bind(slot) 81 | 82 | // It doesn't error 83 | err := mTable.Render() 84 | r.NoError(err) 85 | 86 | // It handled the case that there are no resources 87 | r.True(NoResourcesCalled) 88 | 89 | // It didn't returned after handling no resources 90 | r.Equal(fakeTable.RenderHeaderCallCount(), 0) 91 | r.Equal(fakeTable.RenderRowCallCount(), 0) 92 | 93 | }) 94 | 95 | } 96 | 97 | //TODO: Revisit after validation logic is fixed for mounts_table 98 | // func TestMountsTable_Fail(t *testing.T) { 99 | // r := require.New(t) 100 | 101 | // t.Run("When SelectDeployment is not set", func(t *testing.T) { 102 | // mt := component.NewMountsTable() 103 | 104 | // mt.Props.HandleNoResources = func(format string, args ...interface{}) {} 105 | 106 | // slot := tview.NewFlex() 107 | // mt.Bind(slot) 108 | 109 | // // It doesn't error 110 | // err := mt.Render() 111 | // r.NoError(err) 112 | 113 | // // It provides the correct error message 114 | // r.EqualError(err, "component properties not set") 115 | 116 | // // It is the correct error 117 | // r.True(errors.Is(err, component.ErrComponentPropsNotSet)) 118 | // }) 119 | // } 120 | -------------------------------------------------------------------------------- /tui/component/namespace_table.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | "github.com/rs/zerolog" 7 | 8 | "github.com/dkyanakiev/vaulty/internal/models" 9 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 10 | "github.com/dkyanakiev/vaulty/tui/styles" 11 | ) 12 | 13 | const TableTitleNamespaces = "Namespaces" 14 | 15 | var ( 16 | TableHeaderNamespaces = []string{ 17 | LabelName, 18 | } 19 | ) 20 | 21 | type SelectNsPathFunc func(ns string) 22 | 23 | type NamespaceTable struct { 24 | Table Table 25 | Props *NamespacesProps 26 | Logger *zerolog.Logger 27 | slot *tview.Flex 28 | } 29 | 30 | type NamespacesProps struct { 31 | SelectedNamespace string 32 | SelectNs SelectNsPathFunc 33 | HandleNoResources models.HandlerFunc 34 | Data []string 35 | } 36 | 37 | func NewNamespaceTable() *NamespaceTable { 38 | t := primitive.NewTable() 39 | 40 | return &NamespaceTable{ 41 | Table: t, 42 | Props: &NamespacesProps{}, 43 | } 44 | } 45 | 46 | func (n *NamespaceTable) Bind(slot *tview.Flex) { 47 | slot.SetTitle("Namespaces") 48 | n.slot = slot 49 | } 50 | 51 | func (n *NamespaceTable) Render() error { 52 | if n.slot == nil { 53 | return ErrComponentNotBound 54 | } 55 | 56 | if n.Props.HandleNoResources == nil { 57 | return ErrComponentPropsNotSet 58 | } 59 | 60 | n.reset() 61 | n.Logger.Debug().Msgf("rendering namespaces: %v", n.Props.Data) 62 | if len(n.Props.Data) == 0 { 63 | n.Props.HandleNoResources( 64 | "%sno namespaces available\n¯%s\\_( ͡• ͜ʖ ͡•)_/¯", 65 | styles.HighlightPrimaryTag, 66 | styles.HighlightSecondaryTag, 67 | ) 68 | 69 | return nil 70 | } 71 | 72 | n.Table.SetTitle(TableTitleNamespaces) 73 | n.Table.RenderHeader(TableHeaderNamespaces) 74 | n.Table.SetSelectedFunc(n.namespaceSelected) 75 | n.renderRows() 76 | 77 | n.slot.AddItem(n.Table.Primitive(), 0, 1, false) 78 | return nil 79 | } 80 | 81 | func (n *NamespaceTable) reset() { 82 | n.Table.Clear() 83 | n.slot.Clear() 84 | } 85 | 86 | func (n *NamespaceTable) renderRows() { 87 | index := 0 88 | for i, ns := range n.Props.Data { 89 | row := []string{ 90 | ns, 91 | } 92 | 93 | index = i + 1 94 | n.Table.RenderRow(row, index, tcell.ColorWhite) 95 | } 96 | } 97 | 98 | func (n *NamespaceTable) GetIDForSelection() string { 99 | row, _ := n.Table.GetSelection() 100 | return n.Table.GetCellContent(row, 0) 101 | } 102 | 103 | func (n *NamespaceTable) namespaceSelected(row, _ int) { 104 | ns := n.Table.GetCellContent(row, 0) 105 | n.Props.SelectedNamespace = ns 106 | } 107 | -------------------------------------------------------------------------------- /tui/component/policy_acl_table.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/internal/models" 5 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 6 | "github.com/dkyanakiev/vaulty/tui/styles" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | var ( 11 | PolicyAclTableHeaders = []string{ 12 | "ACL", 13 | } 14 | ) 15 | 16 | type SelectPolicyACLFunc func(policyName string) 17 | 18 | type PolicyAclTable struct { 19 | TextView TextView 20 | Props *PolicyAclTableProps 21 | Flex *tview.Flex 22 | 23 | slot *tview.Flex 24 | } 25 | 26 | type PolicyAclTableProps struct { 27 | SelectedPolicyName string 28 | // TODO: Might use data? 29 | SelectedPolicyACL string 30 | SelectPath SelectPolicyFunc 31 | HandleNoResources models.HandlerFunc 32 | 33 | Data []string 34 | Namespace string 35 | } 36 | 37 | func NewPolicyAclTable() *PolicyAclTable { 38 | t := primitive.NewTextView(1) 39 | t.SetTextAlign(tview.AlignLeft) 40 | t.SetBorderColor(styles.TcellColorStandard) 41 | t.SetBorder(true) 42 | 43 | flex := tview.NewFlex(). 44 | //(t, 0, 1, true). 45 | AddItem(tview.NewBox(), 0, 1, false) 46 | 47 | pt := &PolicyAclTable{ 48 | Flex: flex, 49 | TextView: t, 50 | Props: &PolicyAclTableProps{}, 51 | } 52 | 53 | return pt 54 | } 55 | 56 | func (p *PolicyAclTable) Bind(slot *tview.Flex) { 57 | p.slot = slot 58 | } 59 | 60 | func (p *PolicyAclTable) reset() { 61 | p.slot.Clear() 62 | p.TextView.Clear() 63 | } 64 | 65 | func (p *PolicyAclTable) Render() error { 66 | p.reset() 67 | //p.Table.RenderHeader(PolicyAclTableHeaders) 68 | 69 | if p.Props.SelectedPolicyACL == "" { 70 | p.Props.HandleNoResources( 71 | "%sCant read ACL policy \n%s\\(╯°□°)╯︵ ┻━┻", 72 | styles.HighlightPrimaryTag, 73 | styles.HighlightSecondaryTag, 74 | ) 75 | return nil 76 | } 77 | 78 | p.renderACL() 79 | p.slot.AddItem(p.TextView.Primitive(), 0, 1, false) 80 | return nil 81 | } 82 | 83 | func (p *PolicyAclTable) renderACL() { 84 | p.TextView.SetTitle(p.Props.SelectedPolicyName) 85 | p.TextView.SetText(p.Props.SelectedPolicyACL) 86 | } 87 | -------------------------------------------------------------------------------- /tui/component/policy_acl_table_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/tui/component" 7 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 8 | "github.com/dkyanakiev/vaulty/tui/styles" 9 | "github.com/rivo/tview" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestPolicyACLTable_Pass(t *testing.T) { 14 | r := require.New(t) 15 | t.Run("Data to be rendered", func(t *testing.T) { 16 | // fakeTable := &componentfakes.FakeTable{} 17 | fakeTextView := &componentfakes.FakeTextView{} 18 | pt := component.NewPolicyAclTable() 19 | 20 | pt.TextView = fakeTextView 21 | pt.Props.SelectedPolicyName = "policy-one" 22 | pt.Props.SelectedPolicyACL = "path \"secret/data/path\" { capabilities = [\"read\", \"list\"] }" 23 | 24 | pt.Props.SelectPath = func(id string) {} 25 | pt.Props.HandleNoResources = func(format string, args ...interface{}) {} 26 | 27 | slot := tview.NewFlex() 28 | pt.Bind(slot) 29 | 30 | err := pt.Render() 31 | r.NoError(err) 32 | 33 | // It renders the correct text 34 | fakeTextView.GetTextReturns(pt.Props.SelectedPolicyACL) 35 | fakeTextView.GetTextReturnsOnCall(0, pt.Props.SelectedPolicyACL) 36 | renderedText := fakeTextView.GetText(true) 37 | 38 | r.Equal("path \"secret/data/path\" { capabilities = [\"read\", \"list\"] }", renderedText) 39 | 40 | }) 41 | 42 | t.Run("No data to be rendered", func(t *testing.T) { 43 | fakeTextView := &componentfakes.FakeTextView{} 44 | pt := component.NewPolicyAclTable() 45 | 46 | pt.TextView = fakeTextView 47 | pt.Props.SelectedPolicyName = "policy-one" 48 | pt.Props.SelectedPolicyACL = "" 49 | 50 | var handleNoResourcesCalled bool 51 | pt.Props.HandleNoResources = func(format string, args ...interface{}) { 52 | handleNoResourcesCalled = true 53 | 54 | r.Equal("%sCant read ACL policy \n%s\\(╯°□°)╯︵ ┻━┻", format) 55 | r.Len(args, 2) 56 | r.Equal(args[0], styles.HighlightPrimaryTag) 57 | r.Equal(args[1], styles.HighlightSecondaryTag) 58 | } 59 | 60 | slot := tview.NewFlex() 61 | pt.Bind(slot) 62 | 63 | err := pt.Render() 64 | r.NoError(err) 65 | 66 | r.True(handleNoResourcesCalled) 67 | 68 | }) 69 | } 70 | 71 | func TestPolicyACLTable_Fail(t *testing.T) { 72 | } 73 | -------------------------------------------------------------------------------- /tui/component/policy_table.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/internal/models" 5 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 6 | "github.com/dkyanakiev/vaulty/tui/styles" 7 | "github.com/gdamore/tcell/v2" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | const ( 12 | PolicyTableTitle = "Vault ACL Policy List" 13 | ) 14 | 15 | var ( 16 | PolicyTableHeaderJobs = []string{ 17 | "PolicyName", 18 | } 19 | ) 20 | 21 | type SelectPolicyFunc func(policyName string) 22 | 23 | type PolicyTable struct { 24 | Table Table 25 | Props *PolicyTableProps 26 | 27 | slot *tview.Flex 28 | } 29 | 30 | type PolicyTableProps struct { 31 | SelectedPolicyName string 32 | SelectPath SelectPolicyFunc 33 | HandleNoResources models.HandlerFunc 34 | 35 | Data []string 36 | Namespace string 37 | } 38 | 39 | func NewPolicyTable() *PolicyTable { 40 | t := primitive.NewTable() 41 | pt := &PolicyTable{ 42 | Table: t, 43 | Props: &PolicyTableProps{}, 44 | } 45 | 46 | return pt 47 | } 48 | 49 | func (p *PolicyTable) Bind(slot *tview.Flex) { 50 | p.slot = slot 51 | } 52 | 53 | func (p *PolicyTable) reset() { 54 | p.slot.Clear() 55 | p.Table.Clear() 56 | } 57 | 58 | func (p *PolicyTable) Render() error { 59 | p.reset() 60 | 61 | p.Table.SetTitle("Vault ACL Policies") 62 | 63 | if len(p.Props.Data) == 0 { 64 | p.Props.HandleNoResources( 65 | "%sNo policy found\n%s\\(╯°□°)╯︵ ┻━┻", 66 | styles.HighlightPrimaryTag, 67 | styles.HighlightSecondaryTag, 68 | ) 69 | return nil 70 | } 71 | 72 | p.Table.SetSelectedFunc(p.policySelected) 73 | p.Table.RenderHeader(PolicyTableHeaderJobs) 74 | p.renderRows() 75 | 76 | p.slot.AddItem(p.Table.Primitive(), 0, 1, false) 77 | 78 | return nil 79 | 80 | } 81 | 82 | func (p *PolicyTable) GetIDForSelection() string { 83 | row, _ := p.Table.GetSelection() 84 | return p.Table.GetCellContent(row, 0) 85 | } 86 | 87 | func (p *PolicyTable) policySelected(row, _ int) { 88 | path := p.Table.GetCellContent(row, 0) 89 | p.Props.SelectedPolicyName = path 90 | } 91 | 92 | func (p *PolicyTable) renderRows() { 93 | 94 | for i, policy := range p.Props.Data { 95 | row := []string{ 96 | policy, 97 | } 98 | index := i + 1 99 | c := tcell.ColorYellow 100 | 101 | p.Table.RenderRow(row, index, c) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tui/component/policy_table_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/tui/component" 7 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 8 | "github.com/dkyanakiev/vaulty/tui/styles" 9 | "github.com/gdamore/tcell/v2" 10 | "github.com/rivo/tview" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestPolicyTable_Pass(t *testing.T) { 15 | r := require.New(t) 16 | t.Run("When the component is bound", func(t *testing.T) { 17 | 18 | fakeTable := &componentfakes.FakeTable{} 19 | pt := component.NewPolicyTable() 20 | 21 | pt.Table = fakeTable 22 | pt.Props.Data = []string{"policy1", "policy2", "policy3"} 23 | 24 | pt.Props.SelectPath = func(id string) {} 25 | pt.Props.HandleNoResources = func(format string, args ...interface{}) {} 26 | slot := tview.NewFlex() 27 | pt.Bind(slot) 28 | // It doesn't error 29 | err := pt.Render() 30 | r.NoError(err) 31 | 32 | // Render header rows 33 | renderHeaderCount := fakeTable.RenderHeaderCallCount() 34 | r.Equal(renderHeaderCount, 1) 35 | 36 | // Correct headers 37 | header := fakeTable.RenderHeaderArgsForCall(0) 38 | r.Equal(component.PolicyTableHeaderJobs, header) 39 | 40 | // It renders the correct number of rows 41 | renderRowCallCount := fakeTable.RenderRowCallCount() 42 | r.Equal(renderRowCallCount, 3) 43 | 44 | row1, index1, c1 := fakeTable.RenderRowArgsForCall(0) 45 | row2, index2, c2 := fakeTable.RenderRowArgsForCall(1) 46 | row3, index3, c3 := fakeTable.RenderRowArgsForCall(2) 47 | 48 | expectedRow1 := []string{"policy1"} 49 | expectedRow2 := []string{"policy2"} 50 | expectedRow3 := []string{"policy3"} 51 | 52 | r.Equal(expectedRow1, row1) 53 | r.Equal(expectedRow2, row2) 54 | r.Equal(expectedRow3, row3) 55 | r.Equal(index1, 1) 56 | r.Equal(index2, 2) 57 | r.Equal(index3, 3) 58 | 59 | r.Equal(c1, tcell.ColorYellow) 60 | r.Equal(c2, tcell.ColorYellow) 61 | r.Equal(c3, tcell.ColorYellow) 62 | 63 | }) 64 | 65 | t.Run("No data to render", func(t *testing.T) { 66 | fakeTable := &componentfakes.FakeTable{} 67 | pt := component.NewPolicyTable() 68 | 69 | pt.Table = fakeTable 70 | pt.Props.Data = []string{} 71 | 72 | pt.Props.HandleNoResources = func(format string, args ...interface{}) {} 73 | var NoResourcesCalled bool 74 | pt.Props.HandleNoResources = func(format string, args ...interface{}) { 75 | NoResourcesCalled = true 76 | 77 | r.Equal("%sNo policy found\n%s\\(╯°□°)╯︵ ┻━┻", format) 78 | r.Len(args, 2) 79 | r.Equal(args[0], styles.HighlightPrimaryTag) 80 | r.Equal(args[1], styles.HighlightSecondaryTag) 81 | } 82 | 83 | slot := tview.NewFlex() 84 | pt.Bind(slot) 85 | // It doesn't error 86 | err := pt.Render() 87 | r.NoError(err) 88 | 89 | // It handled the case that there are no resources 90 | r.True(NoResourcesCalled) 91 | 92 | // It didn't returned after handling no resources 93 | r.Equal(fakeTable.RenderHeaderCallCount(), 0) 94 | r.Equal(fakeTable.RenderRowCallCount(), 0) 95 | 96 | }) 97 | 98 | } 99 | -------------------------------------------------------------------------------- /tui/component/search.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | 6 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | const searchPlaceholder = "(hit esc to leave the filter)" 11 | 12 | type SearchField struct { 13 | InputField InputField 14 | Props *SearchFieldProps 15 | slot *tview.Flex 16 | } 17 | 18 | type SearchFieldProps struct { 19 | DoneFunc SetDoneFunc 20 | ChangedFunc func(text string) 21 | } 22 | 23 | func NewSearchField(label string) *SearchField { 24 | sf := &SearchField{} 25 | sf.Props = &SearchFieldProps{} 26 | label = fmt.Sprintf("%s ", label) 27 | sf.InputField = primitive.NewInputField(label, searchPlaceholder) 28 | return sf 29 | } 30 | 31 | func (s *SearchField) Render() error { 32 | if s.Props.DoneFunc == nil || s.Props.ChangedFunc == nil { 33 | return ErrComponentPropsNotSet 34 | } 35 | 36 | if s.slot == nil { 37 | return ErrComponentNotBound 38 | } 39 | 40 | s.InputField.SetDoneFunc(s.Props.DoneFunc) 41 | s.InputField.SetChangedFunc(s.Props.ChangedFunc) 42 | s.slot.AddItem(s.InputField.Primitive(), 0, 2, false) 43 | return nil 44 | } 45 | 46 | func (s *SearchField) Bind(slot *tview.Flex) { 47 | s.slot = slot 48 | } 49 | -------------------------------------------------------------------------------- /tui/component/search_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) HashiCorp, Inc. 2 | // SPDX-License-Identifier: MPL-2.0 3 | 4 | package component_test 5 | 6 | import ( 7 | "errors" 8 | "testing" 9 | 10 | "github.com/dkyanakiev/vaulty/tui/component" 11 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 12 | "github.com/gdamore/tcell/v2" 13 | "github.com/rivo/tview" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestSearch_Pass(t *testing.T) { 18 | r := require.New(t) 19 | 20 | input := &componentfakes.FakeInputField{} 21 | search := component.NewSearchField("test") 22 | search.InputField = input 23 | 24 | var changedCalled bool 25 | search.Props.ChangedFunc = func(text string) { 26 | changedCalled = true 27 | } 28 | 29 | var doneCalled bool 30 | search.Props.DoneFunc = func(key tcell.Key) { 31 | doneCalled = true 32 | } 33 | search.Bind(tview.NewFlex()) 34 | 35 | err := search.Render() 36 | r.NoError(err) 37 | 38 | actualDoneFunc := input.SetDoneFuncArgsForCall(0) 39 | actualChangedFunc := input.SetChangedFuncArgsForCall(0) 40 | 41 | actualChangedFunc("") 42 | actualDoneFunc(tcell.KeyACK) 43 | 44 | r.True(changedCalled) 45 | r.True(doneCalled) 46 | } 47 | 48 | func TestSearch_Fail(t *testing.T) { 49 | r := require.New(t) 50 | 51 | t.Run("When the component isn't bound", func(t *testing.T) { 52 | input := &componentfakes.FakeInputField{} 53 | search := component.NewSearchField("test") 54 | search.InputField = input 55 | search.Props.ChangedFunc = func(text string) {} 56 | search.Props.DoneFunc = func(key tcell.Key) {} 57 | 58 | err := search.Render() 59 | r.Error(err) 60 | 61 | // It provides the correct error message 62 | r.EqualError(err, "component not bound") 63 | 64 | // It is the correct error 65 | r.True(errors.Is(err, component.ErrComponentNotBound)) 66 | }) 67 | 68 | t.Run("When DoneFunc is not set", func(t *testing.T) { 69 | input := &componentfakes.FakeInputField{} 70 | search := component.NewSearchField("test") 71 | search.InputField = input 72 | search.Props.ChangedFunc = func(text string) {} 73 | search.Bind(tview.NewFlex()) 74 | 75 | err := search.Render() 76 | r.Error(err) 77 | 78 | // It provides the correct error message 79 | r.EqualError(err, "component properties not set") 80 | 81 | // It is the correct error 82 | r.True(errors.Is(err, component.ErrComponentPropsNotSet)) 83 | }) 84 | 85 | t.Run("When ChangedFunc is not set", func(t *testing.T) { 86 | input := &componentfakes.FakeInputField{} 87 | search := component.NewSearchField("test") 88 | search.InputField = input 89 | search.Props.DoneFunc = func(key tcell.Key) {} 90 | search.Bind(tview.NewFlex()) 91 | 92 | err := search.Render() 93 | r.Error(err) 94 | 95 | // It provides the correct error message 96 | r.EqualError(err, "component properties not set") 97 | 98 | // It is the correct error 99 | r.True(errors.Is(err, component.ErrComponentPropsNotSet)) 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /tui/component/secret_obj_table_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/gdamore/tcell/v2" 10 | "github.com/hashicorp/vault/api" 11 | "github.com/rivo/tview" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestSecretObjTable_Pass(t *testing.T) { 16 | r := require.New(t) 17 | 18 | t.Run("Render data as table", func(t *testing.T) { 19 | fakeTable := &componentfakes.FakeTable{} 20 | fakeTextView := &componentfakes.FakeTextView{} 21 | st := component.NewSecretObjTable() 22 | 23 | st.Table = fakeTable 24 | st.TextView = fakeTextView 25 | st.ShowJson = false 26 | st.Editable = false 27 | 28 | mockSecret := &api.Secret{ 29 | RequestID: "mockRequestID", 30 | LeaseID: "mockLeaseID", 31 | LeaseDuration: 3600, 32 | Renewable: true, 33 | Data: map[string]interface{}{ 34 | "data": map[string]interface{}{ 35 | "key1": "dZpT6XnlnktMXaYF", 36 | "key2": "10mNsYOLfd1OfohW", 37 | }, 38 | }, 39 | } 40 | 41 | st.Props.Data = mockSecret 42 | 43 | st.Props.SelectPath = func(id string) {} 44 | st.Props.HandleNoResources = func(format string, args ...interface{}) {} 45 | slot := tview.NewFlex() 46 | st.Bind(slot) 47 | // It doesn't error 48 | err := st.Render() 49 | r.NoError(err) 50 | 51 | // Render header rows 52 | renderHeaderCount := fakeTable.RenderHeaderCallCount() 53 | r.Equal(renderHeaderCount, 1) 54 | headers := fakeTable.RenderHeaderArgsForCall(0) 55 | r.Equal(headers, component.SecretObjTableHeaderJobs) 56 | 57 | // Render rows 58 | renderRowCallCount := fakeTable.RenderRowCallCount() 59 | r.Equal(renderRowCallCount, 2) 60 | 61 | row1, index1, c1 := fakeTable.RenderRowArgsForCall(0) 62 | row2, index2, c2 := fakeTable.RenderRowArgsForCall(1) 63 | 64 | expectedRow1 := []string{"key1", "dZpT6XnlnktMXaYF"} 65 | expectedRow2 := []string{"key2", "10mNsYOLfd1OfohW"} 66 | 67 | r.Equal(expectedRow1, row1) 68 | r.Equal(expectedRow2, row2) 69 | r.Equal(index1, 1) 70 | r.Equal(index2, 2) 71 | r.Equal(c1, tcell.ColorYellow) 72 | r.Equal(c2, tcell.ColorYellow) 73 | 74 | }) 75 | 76 | t.Run("Render data as json", func(t *testing.T) { 77 | fakeTable := &componentfakes.FakeTable{} 78 | fakeTextView := &componentfakes.FakeTextView{} 79 | st := component.NewSecretObjTable() 80 | 81 | st.Table = fakeTable 82 | st.TextView = fakeTextView 83 | st.ShowJson = true 84 | st.Editable = false 85 | 86 | mockSecret := &api.Secret{ 87 | RequestID: "mockRequestID", 88 | LeaseID: "mockLeaseID", 89 | LeaseDuration: 3600, 90 | Renewable: true, 91 | Data: map[string]interface{}{ 92 | "data": map[string]interface{}{ 93 | "key1": "dZpT6XnlnktMXaYF", 94 | "key2": "10mNsYOLfd1OfohW", 95 | }, 96 | }, 97 | } 98 | 99 | st.Props.Data = mockSecret 100 | correctText, _ := json.Marshal(mockSecret.Data["data"]) 101 | 102 | st.Props.SelectPath = func(id string) {} 103 | st.Props.HandleNoResources = func(format string, args ...interface{}) {} 104 | slot := tview.NewFlex() 105 | st.Bind(slot) 106 | // It doesn't error 107 | err := st.Render() 108 | r.NoError(err) 109 | 110 | // Renders correct text 111 | 112 | fakeTextView.GetTextReturns(string(correctText)) 113 | renderedText := fakeTextView.GetText(true) 114 | 115 | r.Equal(string(correctText), renderedText) 116 | }) 117 | 118 | t.Run("No data to render", func(t *testing.T) { 119 | fakeTable := &componentfakes.FakeTable{} 120 | fakeTextView := &componentfakes.FakeTextView{} 121 | st := component.NewSecretObjTable() 122 | 123 | st.Table = fakeTable 124 | st.TextView = fakeTextView 125 | st.ShowJson = false 126 | st.Editable = false 127 | 128 | st.Props.Data = nil 129 | 130 | var NoResourcesCalled bool 131 | st.Props.HandleNoResources = func(format string, args ...interface{}) { 132 | NoResourcesCalled = true 133 | } 134 | 135 | slot := tview.NewFlex() 136 | st.Bind(slot) 137 | // It doesn't error 138 | err := st.Render() 139 | r.NoError(err) 140 | 141 | r.True(NoResourcesCalled) 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /tui/component/secrets_table.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/dkyanakiev/vaulty/internal/models" 8 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 9 | "github.com/dkyanakiev/vaulty/tui/styles" 10 | "github.com/gdamore/tcell/v2" 11 | "github.com/rivo/tview" 12 | ) 13 | 14 | const ( 15 | SecretsTableMount = "Secrets " 16 | ) 17 | 18 | var ( 19 | SecretsTableHeaderJobs = []string{ 20 | SecretPath, 21 | SecretObject, 22 | } 23 | ) 24 | 25 | type SelectSecretsPathFunc func(mountPath string) 26 | 27 | type SecretsTable struct { 28 | Table Table 29 | Props *SecretsTableProps 30 | 31 | slot *tview.Flex 32 | } 33 | 34 | type SecretsTableProps struct { 35 | SelectedMount string 36 | SelectedObject string 37 | SelectedPath string 38 | SelectPath SelectMountPathFunc 39 | HandleNoResources models.HandlerFunc 40 | // Data map[string]*models.MountOutput 41 | 42 | Data []models.SecretPath 43 | Namespace string 44 | } 45 | 46 | func NewSecretsTable() *SecretsTable { 47 | t := primitive.NewTable() 48 | 49 | st := &SecretsTable{ 50 | Table: t, 51 | Props: &SecretsTableProps{}, 52 | } 53 | 54 | return st 55 | } 56 | 57 | func (s *SecretsTable) Bind(slot *tview.Flex) { 58 | s.slot = slot 59 | } 60 | 61 | func (s *SecretsTable) reset() { 62 | s.slot.Clear() 63 | s.Table.Clear() 64 | } 65 | 66 | func (s *SecretsTable) pathSelected(row, _ int) { 67 | mountPath := s.Table.GetCellContent(row, 0) 68 | s.Props.SelectedMount = fmt.Sprintf("%s%s", s.Props.SelectedMount, mountPath) 69 | } 70 | 71 | func (s *SecretsTable) GetIDForSelection() (string, string) { 72 | row, _ := s.Table.GetSelection() 73 | name := s.Table.GetCellContent(row, 0) 74 | secret := s.Table.GetCellContent(row, 1) 75 | return name, secret 76 | } 77 | 78 | func (s *SecretsTable) Render() error { 79 | s.reset() 80 | fullPath := fmt.Sprintf("%s%s", s.Props.SelectedMount, s.Props.SelectedPath) 81 | s.Table.SetTitle("%s (%s)", SecretsTableMount, fullPath) 82 | 83 | if len(s.Props.Data) == 0 { 84 | s.Props.HandleNoResources( 85 | "%sno secrets available\n¯%s\\_( ͡• ͜ʖ ͡•)_/¯", 86 | styles.HighlightPrimaryTag, 87 | styles.HighlightSecondaryTag, 88 | ) 89 | 90 | return nil 91 | } 92 | 93 | s.Table.SetSelectedFunc(s.pathSelected) 94 | s.Table.RenderHeader(SecretsTableHeaderJobs) 95 | s.renderRows() 96 | 97 | s.slot.AddItem(s.Table.Primitive(), 0, 1, false) 98 | return nil 99 | } 100 | 101 | func (s *SecretsTable) renderRows() { 102 | 103 | for i, obj := range s.Props.Data { 104 | row := []string{ 105 | obj.PathName, 106 | strconv.FormatBool(obj.IsSecret), 107 | } 108 | index := i + 1 109 | c := tcell.ColorYellow 110 | 111 | s.Table.RenderRow(row, index, c) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tui/component/secrets_table_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/dkyanakiev/vaulty/tui/styles" 10 | "github.com/gdamore/tcell/v2" 11 | "github.com/rivo/tview" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestSecretsTable_Pass(t *testing.T) { 16 | r := require.New(t) 17 | t.Run("When there is data to render", func(t *testing.T) { 18 | 19 | fakeTable := &componentfakes.FakeTable{} 20 | st := component.NewSecretsTable() 21 | 22 | st.Table = fakeTable 23 | st.Props.Namespace = "default" 24 | mockData := []models.SecretPath{ 25 | { 26 | PathName: "mockPathName1", 27 | IsSecret: true, 28 | }, 29 | { 30 | PathName: "mockPathName2", 31 | IsSecret: false, 32 | }, 33 | } 34 | 35 | st.Props.Data = mockData 36 | st.Props.SelectPath = func(id string) {} 37 | st.Props.HandleNoResources = func(format string, args ...interface{}) {} 38 | 39 | slot := tview.NewFlex() 40 | st.Bind(slot) 41 | // It doesn't error 42 | err := st.Render() 43 | r.NoError(err) 44 | 45 | // Render header rows 46 | renderHeaderCount := fakeTable.RenderHeaderCallCount() 47 | r.Equal(renderHeaderCount, 1) 48 | 49 | // Correct headers 50 | header := fakeTable.RenderHeaderArgsForCall(0) 51 | r.Equal(component.SecretsTableHeaderJobs, header) 52 | 53 | // It renders the correct number of rows 54 | renderRowCallCount := fakeTable.RenderRowCallCount() 55 | r.Equal(renderRowCallCount, 2) 56 | 57 | row1, index1, c1 := fakeTable.RenderRowArgsForCall(0) 58 | row2, index2, c2 := fakeTable.RenderRowArgsForCall(1) 59 | expectedRow1 := []string{"mockPathName1", "true"} 60 | expectedRow2 := []string{"mockPathName2", "false"} 61 | 62 | r.Equal(expectedRow1, row1) 63 | r.Equal(expectedRow2, row2) 64 | 65 | r.Equal(index1, 1) 66 | r.Equal(index2, 2) 67 | r.Equal(c1, tcell.ColorYellow) 68 | r.Equal(c2, tcell.ColorYellow) 69 | 70 | }) 71 | 72 | t.Run("No data to render", func(t *testing.T) { 73 | fakeTable := &componentfakes.FakeTable{} 74 | st := component.NewSecretsTable() 75 | 76 | st.Table = fakeTable 77 | st.Props.Namespace = "default" 78 | 79 | st.Props.Data = nil 80 | st.Props.SelectPath = func(id string) {} 81 | st.Props.HandleNoResources = func(format string, args ...interface{}) {} 82 | 83 | var NoResourcesCalled bool 84 | st.Props.HandleNoResources = func(format string, args ...interface{}) { 85 | NoResourcesCalled = true 86 | 87 | r.Equal("%sno secrets available\n¯%s\\_( ͡• ͜ʖ ͡•)_/¯", format) 88 | r.Len(args, 2) 89 | r.Equal(args[0], styles.HighlightPrimaryTag) 90 | r.Equal(args[1], styles.HighlightSecondaryTag) 91 | } 92 | slot := tview.NewFlex() 93 | st.Bind(slot) 94 | // It doesn't error 95 | err := st.Render() 96 | r.NoError(err) 97 | r.True(NoResourcesCalled) 98 | 99 | }) 100 | 101 | } 102 | -------------------------------------------------------------------------------- /tui/component/selections.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | 8 | "github.com/dkyanakiev/vaulty/internal/state" 9 | "github.com/dkyanakiev/vaulty/tui/primitives" 10 | "github.com/dkyanakiev/vaulty/tui/styles" 11 | ) 12 | 13 | var ( 14 | labelNamespaceDropdown = fmt.Sprintf("%sNamespace : ▾ %s", 15 | styles.HighlightSecondaryTag, 16 | styles.StandardColorTag, 17 | ) 18 | ) 19 | 20 | type Selections struct { 21 | Namespace DropDown 22 | Mounts DropDown 23 | //Path 24 | state *state.State 25 | slot *tview.Flex 26 | } 27 | 28 | func NewSelections(state *state.State) *Selections { 29 | return &Selections{ 30 | Namespace: primitives.NewDropDown(labelNamespaceDropdown), 31 | state: state, 32 | } 33 | } 34 | 35 | func (s *Selections) Init() error { 36 | if s.slot == nil { 37 | return ErrComponentNotBound 38 | } 39 | 40 | // if len(s.state.Namespaces) != 0 { 41 | s.Namespace.SetOptions(s.state.Namespaces, s.Selected) 42 | s.Namespace.SetCurrentOption(len(s.state.SelectedNamespace) - 1) 43 | s.Namespace.SetSelectedFunc(s.rerender) 44 | 45 | // } 46 | s.state.Elements.DropDownNamespace = s.Namespace.Primitive().(*tview.DropDown) 47 | s.slot.AddItem(s.Namespace.Primitive(), 0, 1, true) 48 | 49 | return nil 50 | } 51 | func (s *Selections) Render() error { 52 | if s.slot == nil { 53 | return ErrComponentNotBound 54 | } 55 | 56 | // if len(s.state.Namespaces) != 0 { 57 | s.Namespace.SetOptions(s.state.Namespaces, s.Selected) 58 | 59 | // } 60 | s.state.Elements.DropDownNamespace = s.Namespace.Primitive().(*tview.DropDown) 61 | // s.slot.AddItem(s.Namespace.Primitive(), 0, 1, true) 62 | 63 | return nil 64 | } 65 | 66 | func (s *Selections) Refresh() { 67 | s.Render() 68 | } 69 | 70 | // func (s *Selections) Update() error { 71 | // s.Namespace.SetOptions(s.state.Namespaces, s.selected) 72 | // s.Namespace.SetCurrentOption(len(s.state.Namespace) - 1) 73 | // s.Namespace.SetSelectedFunc(s.rerender) 74 | 75 | // } 76 | 77 | func (s *Selections) Bind(slot *tview.Flex) { 78 | s.slot = slot 79 | } 80 | 81 | func (s *Selections) Selected(text string, index int) { 82 | s.state.SelectedNamespace = text 83 | } 84 | 85 | func (s *Selections) rerender(text string, index int) { 86 | s.state.SelectedNamespace = text 87 | } 88 | -------------------------------------------------------------------------------- /tui/component/selector.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/primitives" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | const pageNameSelector = "selector" 10 | 11 | type SelectorModal struct { 12 | Modal Selector 13 | Props *SelectorProps 14 | pages *tview.Pages 15 | keyBindings map[tcell.Key]func() 16 | } 17 | 18 | type SelectorProps struct { 19 | Items []string 20 | AllocationID string 21 | } 22 | 23 | func NewSelectorModal() *SelectorModal { 24 | s := &SelectorModal{ 25 | Modal: primitives.NewSelectionModal(), 26 | Props: &SelectorProps{}, 27 | keyBindings: map[tcell.Key]func(){}, 28 | } 29 | 30 | s.Modal.GetTable().SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { 31 | if fn, ok := s.keyBindings[event.Key()]; ok { 32 | fn() 33 | } 34 | 35 | return event 36 | }) 37 | 38 | return s 39 | } 40 | 41 | func (s *SelectorModal) Render() error { 42 | if s.pages == nil { 43 | return ErrComponentNotBound 44 | } 45 | 46 | if s.Props.Items == nil { 47 | return ErrComponentPropsNotSet 48 | } 49 | 50 | table := s.Modal.GetTable() 51 | table.Clear() 52 | 53 | for i, v := range s.Props.Items { 54 | table.RenderRow([]string{v}, i, tcell.ColorWhite) 55 | } 56 | 57 | s.Modal.GetTable().SetTitle("Select a Task (alloc: %s)", s.Props.AllocationID) 58 | 59 | s.pages.AddPage(pageNameSelector, s.Modal.Container(), true, true) 60 | 61 | return nil 62 | } 63 | 64 | func (s *SelectorModal) Bind(pages *tview.Pages) { 65 | s.pages = pages 66 | } 67 | 68 | func (s *SelectorModal) SetSelectedFunc(fn func(task string)) { 69 | s.Modal.GetTable().SetSelectedFunc(func(row, column int) { 70 | task := s.Modal.GetTable().GetCellContent(row, 0) 71 | fn(task) 72 | s.Close() 73 | }) 74 | } 75 | 76 | func (s *SelectorModal) Close() { 77 | s.pages.RemovePage(pageNameSelector) 78 | } 79 | 80 | func (s *SelectorModal) BindKey(key tcell.Key, fn func()) { 81 | s.keyBindings[key] = fn 82 | } 83 | -------------------------------------------------------------------------------- /tui/component/text_input.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/primitives" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | const textPlaceholder = "(hit enter or esc to leave)" 9 | 10 | type TextInfoInput struct { 11 | InputField InputField 12 | Props *TextInfoInputProps 13 | slot *tview.Flex 14 | } 15 | 16 | type TextInfoInputProps struct { 17 | DoneFunc SetDoneFunc 18 | } 19 | 20 | func NewTextInfoInput() *TextInfoInput { 21 | ti := &TextInfoInput{} 22 | ti.Props = &TextInfoInputProps{} 23 | 24 | in := primitives.NewInputField("name: ", textPlaceholder) 25 | 26 | ti.InputField = in 27 | 28 | return ti 29 | } 30 | 31 | func (ti *TextInfoInput) Render() error { 32 | if ti.Props.DoneFunc == nil { 33 | return ErrComponentPropsNotSet 34 | } 35 | 36 | if ti.slot == nil { 37 | return ErrComponentNotBound 38 | } 39 | ti.InputField.SetDoneFunc(ti.Props.DoneFunc) 40 | ti.InputField.SetDoneFunc(ti.Props.DoneFunc) 41 | ti.slot.AddItem(ti.InputField.Primitive(), 0, 2, false) 42 | return nil 43 | } 44 | 45 | func (ti *TextInfoInput) Bind(slot *tview.Flex) { 46 | ti.slot = slot 47 | } 48 | -------------------------------------------------------------------------------- /tui/component/toggles.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | primitive "github.com/dkyanakiev/vaulty/tui/primitives" 8 | "github.com/dkyanakiev/vaulty/tui/styles" 9 | "github.com/rivo/tview" 10 | "github.com/rs/zerolog" 11 | ) 12 | 13 | type TogglesInfo struct { 14 | TextView TextView 15 | Props *TogglesInfoProps 16 | Logger *zerolog.Logger 17 | 18 | slot *tview.Flex 19 | } 20 | 21 | type TogglesInfoProps struct { 22 | Info string 23 | Namespace string 24 | FilterText string "" 25 | Editable bool 26 | } 27 | 28 | func NewTogglesInfo() *TogglesInfo { 29 | return &TogglesInfo{ 30 | TextView: primitive.NewTextView(tview.AlignLeft), 31 | Props: &TogglesInfoProps{}, 32 | } 33 | } 34 | 35 | func (t *TogglesInfo) InitialRender(ns string) error { 36 | if t.slot == nil { 37 | return ErrComponentNotBound 38 | } 39 | editableStr := strconv.FormatBool(t.Props.Editable) 40 | 41 | text := fmt.Sprintf( 42 | "\n%sNamespace: %s %s \n%sEdit Mode: %s %s \n%sFilter: %s %s \n", 43 | styles.HighlightSecondaryTag, 44 | styles.StandardColorTag, 45 | ns, 46 | styles.HighlightSecondaryTag, 47 | styles.StandardColorTag, 48 | editableStr, 49 | styles.HighlightSecondaryTag, 50 | styles.StandardColorTag, 51 | t.Props.FilterText, 52 | ) 53 | 54 | t.TextView.SetText(text) 55 | t.slot.AddItem(t.TextView.Primitive(), 0, 1, false) 56 | 57 | return nil 58 | } 59 | 60 | func (t *TogglesInfo) Render() error { 61 | if t.slot == nil { 62 | return ErrComponentNotBound 63 | } 64 | editableStr := strconv.FormatBool(t.Props.Editable) 65 | 66 | text := fmt.Sprintf( 67 | "\n%sNamespace: %s %s \n%sEdit Mode: %s %s \n%sFilter: %s %s \n", 68 | styles.HighlightSecondaryTag, 69 | styles.StandardColorTag, 70 | t.Props.Namespace, 71 | styles.HighlightSecondaryTag, 72 | styles.StandardColorTag, 73 | editableStr, 74 | styles.HighlightSecondaryTag, 75 | styles.StandardColorTag, 76 | t.Props.FilterText, 77 | ) 78 | 79 | t.TextView.SetText(text) 80 | //t.slot.AddItem(t.TextView.Primitive(), 0, 1, false) 81 | 82 | return nil 83 | } 84 | 85 | func (t *TogglesInfo) Bind(slot *tview.Flex) { 86 | t.slot = slot 87 | } 88 | -------------------------------------------------------------------------------- /tui/component/vaultinfo.go: -------------------------------------------------------------------------------- 1 | package component 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/primitives" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | type VaultInfo struct { 9 | TextView TextView 10 | Props *VaultInfoProps 11 | 12 | slot *tview.Flex 13 | } 14 | 15 | type VaultInfoProps struct { 16 | Info string 17 | } 18 | 19 | func NewVaultInfo() *VaultInfo { 20 | return &VaultInfo{ 21 | TextView: primitives.NewTextView(tview.AlignLeft), 22 | Props: &VaultInfoProps{}, 23 | } 24 | } 25 | 26 | func (c *VaultInfo) InitialRender() error { 27 | if c.slot == nil { 28 | return ErrComponentNotBound 29 | } 30 | 31 | c.TextView.SetText(c.Props.Info) 32 | c.slot.AddItem(c.TextView.Primitive(), 0, 1, false) 33 | 34 | return nil 35 | } 36 | 37 | func (c *VaultInfo) Render() error { 38 | if c.slot == nil { 39 | return ErrComponentNotBound 40 | } 41 | 42 | c.TextView.SetText(c.Props.Info) 43 | // c.slot.AddItem(c.TextView.Primitive(), 0, 1, false) 44 | 45 | return nil 46 | } 47 | 48 | func (c *VaultInfo) Bind(slot *tview.Flex) { 49 | c.slot = slot 50 | } 51 | -------------------------------------------------------------------------------- /tui/component/vaultinfo_test.go: -------------------------------------------------------------------------------- 1 | package component_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/component/componentfakes" 9 | "github.com/rivo/tview" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestVaultInfo_Pass(t *testing.T) { 14 | r := require.New(t) 15 | 16 | textView := &componentfakes.FakeTextView{} 17 | vaultInfo := component.NewVaultInfo() 18 | vaultInfo.TextView = textView 19 | 20 | vaultInfo.Props.Info = "info" 21 | 22 | vaultInfo.Bind(tview.NewFlex()) 23 | 24 | err := vaultInfo.Render() 25 | r.NoError(err) 26 | 27 | text := textView.SetTextArgsForCall(0) 28 | r.Equal(text, "info") 29 | } 30 | 31 | func TestVaultInfo_Failt(t *testing.T) { 32 | r := require.New(t) 33 | 34 | textView := &componentfakes.FakeTextView{} 35 | vaultInfo := component.NewVaultInfo() 36 | vaultInfo.TextView = textView 37 | vaultInfo.Props.Info = "info" 38 | 39 | err := vaultInfo.Render() 40 | r.Error(err) 41 | 42 | r.True(errors.Is(err, component.ErrComponentNotBound)) 43 | r.EqualError(err, "component not bound") 44 | } 45 | -------------------------------------------------------------------------------- /tui/layout/layout.go: -------------------------------------------------------------------------------- 1 | package layout 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | const NameMainPage = "main" 8 | const NameErrorPage = "error" 9 | 10 | type Layout struct { 11 | Container *tview.Application 12 | 13 | Pages *tview.Pages 14 | MainPage *tview.Flex 15 | 16 | Header *Header 17 | Body *tview.Flex 18 | Footer *tview.Flex 19 | 20 | Elements *Elements 21 | } 22 | 23 | type Elements struct { 24 | ClusterInfo *tview.Flex 25 | Dropdowns *tview.Flex 26 | } 27 | 28 | type Header struct { 29 | SlotInfo *tview.Flex 30 | SlotCmd *tview.Flex 31 | SlotLogo *tview.Flex 32 | } 33 | 34 | func EnableMouse(l *Layout) { 35 | l.Container.EnableMouse(true) 36 | } 37 | 38 | func New(options ...func(*Layout)) *Layout { 39 | v := &Layout{} 40 | 41 | for _, opt := range options { 42 | opt(v) 43 | } 44 | 45 | return v 46 | } 47 | 48 | func Default(l *Layout) { 49 | l.Header = &Header{} 50 | l.Elements = &Elements{} 51 | 52 | l.Elements.ClusterInfo = tview.NewFlex().SetDirection(tview.FlexRow) 53 | l.Elements.Dropdowns = tview.NewFlex().SetDirection(tview.FlexRow) 54 | 55 | l.Header.SlotInfo = tview.NewFlex().SetDirection(tview.FlexRow) 56 | l.Header.SlotInfo.AddItem(l.Elements.ClusterInfo, 0, 1, false) 57 | l.Header.SlotInfo.AddItem(l.Elements.Dropdowns, 1, 1, false) 58 | 59 | l.Header.SlotCmd = tview.NewFlex() 60 | l.Header.SlotLogo = tview.NewFlex() 61 | 62 | header := tview.NewFlex(). 63 | AddItem(l.Header.SlotInfo, 0, 1, false). 64 | AddItem(l.Header.SlotCmd, 0, 1, false). 65 | AddItem(l.Header.SlotLogo, 0, 1, false) 66 | 67 | header.SetBorderPadding(1, 1, 2, 2) 68 | 69 | footer := tview.NewFlex() 70 | body := tview.NewFlex() 71 | 72 | mainPage := tview.NewFlex().SetDirection(tview.FlexRow) 73 | mainPage. 74 | AddItem(header, 0, 4, false). 75 | AddItem(body, 0, 12, false). 76 | AddItem(footer, 0, 0, false) 77 | 78 | pages := tview.NewPages() 79 | pages.AddPage(NameMainPage, mainPage, true, true) 80 | //pages.AddPage("PolicyAclTable", , true, true) 81 | 82 | l.Body = body 83 | l.Footer = footer 84 | 85 | l.MainPage = mainPage 86 | l.Pages = pages 87 | 88 | l.Container = tview.NewApplication(). 89 | SetRoot(pages, true). 90 | SetFocus(pages) 91 | 92 | } 93 | -------------------------------------------------------------------------------- /tui/layout/layout_test.go: -------------------------------------------------------------------------------- 1 | package layout_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/dkyanakiev/vaulty/tui/layout" 7 | "github.com/rivo/tview" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDefaultLayout(t *testing.T) { 12 | r := require.New(t) 13 | 14 | l := layout.New(layout.Default) 15 | 16 | r.NotNil(l.Container) 17 | r.IsType(l.Container, &tview.Application{}) 18 | 19 | r.NotNil(l.Pages) 20 | r.IsType(l.Pages, &tview.Pages{}) 21 | r.Equal(l.Pages.GetPageCount(), 1) 22 | r.True(l.Pages.HasPage("main")) 23 | 24 | r.NotNil(l.Header) 25 | r.NotNil(l.Header.SlotInfo) 26 | r.IsType(l.Header.SlotInfo, &tview.Flex{}) 27 | 28 | r.NotNil(l.Header.SlotCmd) 29 | r.IsType(l.Header.SlotCmd, &tview.Flex{}) 30 | 31 | r.NotNil(l.Header.SlotLogo) 32 | r.IsType(l.Header.SlotLogo, &tview.Flex{}) 33 | 34 | r.NotNil(l.Elements) 35 | r.NotNil(l.Elements.ClusterInfo) 36 | r.IsType(l.Elements.ClusterInfo, &tview.Flex{}) 37 | 38 | r.NotNil(l.Elements.Dropdowns) 39 | r.IsType(l.Elements.Dropdowns, &tview.Flex{}) 40 | 41 | r.NotNil(l.Body) 42 | r.IsType(l.Body, &tview.Flex{}) 43 | 44 | r.NotNil(l.Footer) 45 | r.IsType(l.Footer, &tview.Flex{}) 46 | 47 | r.NotNil(l.MainPage) 48 | r.IsType(l.MainPage, &tview.Flex{}) 49 | } 50 | -------------------------------------------------------------------------------- /tui/primitives/box.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import "github.com/rivo/tview" 4 | 5 | type Box struct { 6 | *tview.Box 7 | } 8 | 9 | func NewBox() *Box { 10 | b := tview.NewBox() 11 | 12 | return &Box{Box: b} 13 | } 14 | 15 | func (b *Box) Primitive() tview.Primitive { 16 | return b.Box 17 | } 18 | -------------------------------------------------------------------------------- /tui/primitives/dropdown.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | 6 | "github.com/dkyanakiev/vaulty/tui/styles" 7 | ) 8 | 9 | type DropDown struct { 10 | primitive *tview.DropDown 11 | } 12 | 13 | func NewDropDown(label string) *DropDown { 14 | dd := tview.NewDropDown() 15 | dd.SetLabel(label) 16 | dd.SetBackgroundColor(styles.TcellBackgroundColor) 17 | dd.SetCurrentOption(0) 18 | dd.SetFieldBackgroundColor(styles.TcellBackgroundColor) 19 | dd.SetFieldTextColor(styles.TcellColorStandard) 20 | 21 | return &DropDown{dd} 22 | } 23 | 24 | func (d *DropDown) SetOptions(options []string, selected func(text string, index int)) { 25 | d.primitive.SetOptions(options, selected) 26 | } 27 | 28 | func (d *DropDown) SetCurrentOption(index int) { 29 | d.primitive.SetCurrentOption(index) 30 | } 31 | 32 | func (d *DropDown) SetSelectedFunc(selected func(text string, index int)) { 33 | d.primitive.SetSelectedFunc(selected) 34 | } 35 | 36 | func (d *DropDown) Primitive() tview.Primitive { 37 | return d.primitive 38 | } 39 | -------------------------------------------------------------------------------- /tui/primitives/form.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | type Form struct { 9 | primitive *tview.Form 10 | container *tview.Flex 11 | } 12 | 13 | func NewForm(title string, c tcell.Color) *Form { 14 | f := tview.NewForm() 15 | f.SetTitle(title) 16 | f.SetTitleAlign(tview.AlignCenter) 17 | f.SetBackgroundColor(c) 18 | f.SetFieldTextColor(tcell.ColorBlack) 19 | 20 | flex := tview.NewFlex(). 21 | AddItem(nil, 0, 1, false). 22 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 23 | AddItem(nil, 0, 1, false). 24 | AddItem(f, 10, 1, true). 25 | AddItem(nil, 0, 1, false), 80, 1, false). 26 | AddItem(nil, 0, 1, false) 27 | 28 | return &Form{ 29 | primitive: f, 30 | container: flex, 31 | } 32 | } 33 | 34 | func (f *Form) Container() tview.Primitive { 35 | return f.container 36 | } 37 | 38 | func (f *Form) Primitive() tview.Primitive { 39 | return f.primitive 40 | } 41 | -------------------------------------------------------------------------------- /tui/primitives/input.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | type InputField struct { 9 | primitive *tview.InputField 10 | } 11 | 12 | func NewInputField(label, placeholder string) *InputField { 13 | i := tview.NewInputField() 14 | i.SetLabel(label) 15 | i.SetFieldWidth(0) 16 | i.SetAcceptanceFunc(tview.InputFieldMaxLength(40)) 17 | i.SetPlaceholder(placeholder) 18 | i.SetBorder(true) 19 | i.SetFieldBackgroundColor(tcell.NewRGBColor(40, 44, 48)) 20 | i.SetBackgroundColor(tcell.NewRGBColor(40, 44, 48)) 21 | i.SetBorderAttributes(tcell.AttrDim) 22 | 23 | return &InputField{i} 24 | } 25 | 26 | func (i *InputField) SetDoneFunc(handler func(k tcell.Key)) { 27 | i.primitive.SetDoneFunc(handler) 28 | } 29 | 30 | func (i *InputField) SetChangedFunc(handler func(text string)) { 31 | i.primitive.SetChangedFunc(handler) 32 | } 33 | 34 | func (i *InputField) SetText(text string) { 35 | i.primitive.SetText(text) 36 | } 37 | 38 | func (i *InputField) GetText() string { 39 | return i.primitive.GetText() 40 | } 41 | 42 | func (i *InputField) SetAutocompleteFunc(callback func(currentText string) (entries []string)) { 43 | i.primitive.SetAutocompleteFunc(callback) 44 | } 45 | 46 | func (i *InputField) Primitive() tview.Primitive { 47 | return i.primitive 48 | } 49 | -------------------------------------------------------------------------------- /tui/primitives/modal.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | "github.com/rivo/tview" 6 | ) 7 | 8 | type Modal struct { 9 | primitive *tview.Modal 10 | container *tview.Flex 11 | } 12 | 13 | func NewModal(title string, buttons []string, c tcell.Color) *Modal { 14 | m := tview.NewModal() 15 | m.SetTitle(title) 16 | m.SetTitleAlign(tview.AlignCenter) 17 | m.SetBackgroundColor(c) 18 | m.SetTextColor(tcell.ColorBlack) 19 | m.AddButtons(buttons) 20 | 21 | f := tview.NewFlex(). 22 | AddItem(nil, 0, 1, false). 23 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 24 | AddItem(nil, 0, 1, false). 25 | AddItem(m, 10, 1, true). 26 | AddItem(nil, 0, 1, false), 80, 1, false). 27 | AddItem(nil, 0, 1, false) 28 | 29 | return &Modal{ 30 | primitive: m, 31 | container: f, 32 | } 33 | } 34 | 35 | func (m *Modal) SetDoneFunc(handler func(buttonIndex int, buttonLabel string)) { 36 | m.primitive.SetDoneFunc(handler) 37 | } 38 | 39 | func (m *Modal) SetText(text string) { 40 | m.primitive.SetText(text) 41 | } 42 | 43 | func (m *Modal) SetFocus(index int) { 44 | m.primitive.SetFocus(index) 45 | } 46 | 47 | func (m *Modal) Container() tview.Primitive { 48 | return m.container 49 | } 50 | 51 | func (m *Modal) Primitive() tview.Primitive { 52 | return m.primitive 53 | } 54 | -------------------------------------------------------------------------------- /tui/primitives/selector.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/rivo/tview" 5 | ) 6 | 7 | type SelectionModal struct { 8 | Table *Table 9 | container *tview.Flex 10 | } 11 | 12 | func NewSelectionModal() *SelectionModal { 13 | t := NewTable() 14 | f := tview.NewFlex(). 15 | AddItem(nil, 0, 1, false). 16 | AddItem(tview.NewFlex().SetDirection(tview.FlexRow). 17 | AddItem(nil, 0, 1, false). 18 | AddItem(t.primitive, 10, 1, false). 19 | AddItem(nil, 0, 1, false), 80, 1, false). 20 | AddItem(nil, 0, 1, false) 21 | 22 | return &SelectionModal{ 23 | Table: t, 24 | container: f, 25 | } 26 | } 27 | 28 | func (s *SelectionModal) Container() tview.Primitive { 29 | return s.container 30 | } 31 | 32 | func (s *SelectionModal) Primitive() tview.Primitive { 33 | return s.Table.primitive 34 | } 35 | 36 | func (s *SelectionModal) GetTable() *Table { 37 | return s.Table 38 | } 39 | -------------------------------------------------------------------------------- /tui/primitives/table.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | "github.com/rivo/tview" 8 | 9 | "github.com/dkyanakiev/vaulty/tui/styles" 10 | ) 11 | 12 | // Table is a wrapper of a tview.Table primitive. 13 | type Table struct { 14 | primitive *tview.Table 15 | color tcell.Color 16 | tviewTable *tview.Table 17 | } 18 | 19 | func NewTable() *Table { 20 | t := tview.NewTable() 21 | t.SetBorder(true) 22 | t.SetTitleColor(styles.TcellColorHighlighPrimary) 23 | t.SetSelectable(true, false) 24 | t.SetFixed(1, 1) 25 | t.SetBorderPadding(0, 0, 1, 1) 26 | t.SetBorderColor(styles.TcellColorStandard) 27 | 28 | return &Table{ 29 | primitive: t, 30 | } 31 | } 32 | 33 | func (t *Table) RenderHeader(data []string) { 34 | for i, h := range data { 35 | c := tcell.GetColor(styles.StandardColorHex) 36 | t.primitive.SetCell(0, i, tview.NewTableCell(h). 37 | SetTextColor(c). 38 | SetSelectable(false), 39 | ) 40 | } 41 | } 42 | 43 | func (t *Table) SetTitle(format string, args ...interface{}) { 44 | t.primitive.SetTitle(fmt.Sprintf(format, args...)) 45 | } 46 | 47 | func (t *Table) GetCellContent(row, column int) string { 48 | cell := t.primitive.GetCell(row, column) 49 | return cell.Text 50 | } 51 | 52 | func (t *Table) GetSelection() (row, column int) { 53 | return t.primitive.GetSelection() 54 | } 55 | 56 | func (t *Table) Clear() { 57 | t.primitive.Clear() 58 | } 59 | 60 | func (t *Table) RenderRow(data []string, index int, c tcell.Color) { 61 | for i, r := range data { 62 | t.primitive.SetCell(index, i, 63 | tview.NewTableCell(r).SetTextColor(c).SetExpansion(1), 64 | ) 65 | } 66 | } 67 | 68 | func (t *Table) SetSelectedFunc(fn func(row, column int)) { 69 | t.primitive.SetSelectedFunc(fn) 70 | } 71 | 72 | func (t *Table) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) { 73 | t.primitive.SetInputCapture(capture) 74 | } 75 | 76 | func (t *Table) Primitive() tview.Primitive { 77 | return t.primitive 78 | } 79 | 80 | func (t *Table) ScrollToTop() *tview.Table { 81 | t.primitive.ScrollToBeginning() 82 | return t.primitive 83 | } 84 | 85 | func (t *Table) SetSelectedStyle(style tcell.Style) { 86 | t.tviewTable.SetSelectedStyle(style) 87 | } 88 | 89 | func (t *Table) SetSelectable(rows, columns bool) { 90 | t.primitive.SetSelectable(rows, columns) 91 | } 92 | -------------------------------------------------------------------------------- /tui/primitives/text.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/styles" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | type TextView struct { 10 | primitive *tview.TextView 11 | } 12 | 13 | func NewTextView(align int) *TextView { 14 | t := tview.NewTextView(). 15 | SetDynamicColors(true). 16 | SetTextAlign(align) 17 | box := tview.NewBox() 18 | t.Box = box 19 | t.Box.SetBorderPadding(0, 0, 1, 1) 20 | t.Box.SetTitleColor(styles.TcellColorHighlighPrimary) 21 | t.Box.SetBorderColor(styles.TcellColorStandard) 22 | t.Box.SetBorder(false) 23 | return &TextView{primitive: t} 24 | } 25 | 26 | func (t *TextView) Primitive() tview.Primitive { 27 | return t.primitive 28 | } 29 | 30 | func (t *TextView) ModifyPrimitive(f func(t *tview.TextView)) { 31 | f(t.primitive) 32 | } 33 | 34 | func (t *TextView) SetText(text string) *tview.TextView { 35 | return t.primitive.SetText(text) 36 | } 37 | 38 | func (t *TextView) GetText(wrap bool) string { 39 | return t.primitive.GetText(wrap) 40 | } 41 | 42 | func (t *TextView) SetTitle(title string) { 43 | t.primitive.Box.SetTitle(title) 44 | } 45 | 46 | func (t *TextView) SetBorderColor(color tcell.Color) { 47 | t.primitive.Box.SetBorderColor(color) 48 | } 49 | 50 | func (t *TextView) SetBorder(wrap bool) { 51 | t.primitive.Box.SetBorder(wrap) 52 | } 53 | 54 | func (t *TextView) ScrollToBeginning() *tview.TextView { 55 | return t.primitive.ScrollToBeginning() 56 | } 57 | 58 | func (t *TextView) ScrollToEnd() *tview.TextView { 59 | return t.primitive.ScrollToEnd() 60 | } 61 | 62 | func (t *TextView) Clear() *tview.TextView { 63 | return t.primitive.Clear() 64 | } 65 | 66 | func (t *TextView) Highlight(regionIDs ...string) *tview.TextView { 67 | return t.primitive.Highlight(regionIDs...) 68 | } 69 | 70 | func (t *TextView) SetTextAlign(align int) *tview.TextView { 71 | return t.primitive.SetTextAlign(align) 72 | } 73 | 74 | func (t *TextView) Blur() { 75 | t.primitive.Blur() 76 | } 77 | -------------------------------------------------------------------------------- /tui/primitives/textarea.go: -------------------------------------------------------------------------------- 1 | package primitives 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/styles" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | type TextArea struct { 10 | primitive *tview.TextArea 11 | } 12 | 13 | func NewTextArea() *TextArea { 14 | t := tview.NewTextArea() 15 | box := tview.NewBox() 16 | t.Box = box 17 | t.Box.SetTitleColor(styles.TcellColorHighlighPrimary) 18 | t.Box.SetBorderColor(styles.TcellColorStandard) 19 | t.Box.SetBorder(true) 20 | 21 | return &TextArea{ 22 | primitive: t, 23 | } 24 | } 25 | 26 | func (t *TextArea) Primitive() tview.Primitive { 27 | return t.primitive 28 | } 29 | 30 | func (t *TextArea) SetText(text string, cursorAtEnd bool) *tview.TextArea { 31 | return t.primitive.SetText(text, true) 32 | } 33 | 34 | func (t *TextArea) SetBorder(wrap bool) { 35 | t.primitive.Box.SetBorder(wrap) 36 | 37 | } 38 | 39 | func (t *TextArea) SetTitle(title string) { 40 | t.primitive.Box.SetTitle(title) 41 | } 42 | 43 | func (t *TextArea) SetBorderColor(color tcell.Color) { 44 | t.primitive.Box.SetBorderColor(color) 45 | } 46 | 47 | func (t *TextArea) GetText() string { 48 | return t.primitive.GetText() 49 | } 50 | -------------------------------------------------------------------------------- /tui/styles/styles.go: -------------------------------------------------------------------------------- 1 | package styles 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gdamore/tcell/v2" 7 | ) 8 | 9 | func GetBackgroundColor() tcell.Color { 10 | return tcell.NewRGBColor(40, 44, 48) 11 | } 12 | 13 | var ( 14 | TcellBackgroundColor = tcell.NewRGBColor(40, 44, 48) 15 | 16 | HighlightPrimaryHex = "#26ffe6" 17 | HighlightSecondaryHex = "#baff26" 18 | StandardColorHex = "#00b57c" 19 | ColorActiveHex = "#b3f1ff" 20 | ColorWhiteHex = "#ffffff" 21 | ColorLightGreyHex = "#cccccc" 22 | ColorModalInfoHex = "#61877f" 23 | ColorAttentionHex = "#d98b6a" 24 | 25 | StandardColorTag = fmt.Sprintf("[%s]", StandardColorHex) 26 | HighlightPrimaryTag = fmt.Sprintf("[%s]", HighlightPrimaryHex) 27 | HighlightSecondaryTag = fmt.Sprintf("[%s]", HighlightSecondaryHex) 28 | ColorActiveTag = fmt.Sprintf("[%s]", ColorActiveHex) 29 | ColorWhiteTag = fmt.Sprintf("[%s]", ColorWhiteHex) 30 | ColorLighGreyTag = fmt.Sprintf("[%s]", ColorLightGreyHex) 31 | 32 | TcellColorHighlighPrimary = tcell.GetColor(HighlightPrimaryHex) 33 | TcellColorHighlighSecondary = tcell.GetColor(HighlightSecondaryHex) 34 | TcellColorStandard = tcell.GetColor(StandardColorHex) 35 | TcellColorActive = tcell.GetColor(ColorActiveHex) 36 | TcellColorModalInfo = tcell.GetColor(ColorModalInfoHex) 37 | TcellColorAttention = tcell.GetColor(ColorAttentionHex) 38 | ) 39 | -------------------------------------------------------------------------------- /tui/view/handler.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | func (v *View) handleNoResources(text string, args ...interface{}) { 10 | msg := fmt.Sprintf(text, args...) 11 | info := tview. 12 | NewTextView(). 13 | SetDynamicColors(true). 14 | SetText(msg). 15 | SetTextAlign(tview.AlignCenter) 16 | info.SetBorder(true) 17 | 18 | v.Layout.Body.AddItem(info, 0, 1, false) 19 | } 20 | 21 | func (v *View) err(err error, msg string) { 22 | if err != nil { 23 | msg := fmt.Sprintf("%s: %s", msg, err.Error()) 24 | v.handleError(msg) 25 | } 26 | } 27 | 28 | func (v *View) handleError(format string, args ...interface{}) { 29 | msg := fmt.Sprintf(format, args...) 30 | v.components.Failure.Render(msg) 31 | v.Layout.Container.SetFocus(v.components.Failure.Modal.Primitive()) 32 | } 33 | 34 | func (v *View) handleInfo(format string, args ...interface{}) { 35 | msg := fmt.Sprintf(format, args...) 36 | v.components.Info.Render(msg) 37 | v.Layout.Container.SetFocus(v.components.Info.Modal.Primitive()) 38 | } 39 | 40 | func (v *View) handleFatal(format string, args ...interface{}) { 41 | msg := fmt.Sprintf(format, args...) 42 | v.components.Error.Render(msg) 43 | v.Layout.Container.SetFocus(v.components.Error.Modal.Primitive()) 44 | } 45 | -------------------------------------------------------------------------------- /tui/view/history.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import "github.com/rs/zerolog" 4 | 5 | type History struct { 6 | stack []func() 7 | HistorySize int 8 | Logger *zerolog.Logger 9 | } 10 | 11 | func (h *History) push(back func()) { 12 | h.stack = append(h.stack, back) 13 | if len(h.stack) > h.HistorySize { 14 | h.stack = h.stack[1:] 15 | } 16 | h.Logger.Debug().Msgf("History stack: %v", h.stack) 17 | 18 | } 19 | 20 | func (h *History) pop() { 21 | h.Logger.Debug().Msgf("History stack: %v", h.stack) 22 | if len(h.stack) > 1 { 23 | last := h.stack[len(h.stack)-2] 24 | last() 25 | h.stack = h.stack[:len(h.stack)-2] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /tui/view/init.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/dkyanakiev/vaulty/tui/styles" 9 | "github.com/gdamore/tcell/v2" 10 | ) 11 | 12 | func (v *View) Init(version string) { 13 | // ClusterInfo 14 | v.state.Version = version 15 | v.components.VaultInfo.Props.Info = fmt.Sprintf( 16 | "%sAddress: %s%s %s\n%sVersion:%s %s\n", 17 | styles.HighlightSecondaryTag, 18 | styles.StandardColorTag, 19 | v.state.VaultAddress, 20 | styles.StandardColorTag, 21 | styles.HighlightSecondaryTag, 22 | styles.StandardColorTag, 23 | v.state.VaultVersion, 24 | ) 25 | 26 | v.components.VaultInfo.Bind(v.Layout.Elements.ClusterInfo) 27 | v.components.VaultInfo.InitialRender() 28 | // TogglesInfo 29 | v.components.TogglesInfo.Bind(v.Layout.Elements.ClusterInfo) 30 | v.components.TogglesInfo.InitialRender(v.state.DefaultNamespace) 31 | 32 | // Logo 33 | v.components.Logo.Bind(v.Layout.Header.SlotLogo) 34 | v.components.Logo.Render() 35 | 36 | // Commands 37 | v.components.Commands.Bind(v.Layout.Header.SlotCmd) 38 | v.components.Commands.Render() 39 | 40 | // MountsTable 41 | v.components.MountsTable.Bind(v.Layout.Body) 42 | v.components.MountsTable.Props.HandleNoResources = v.handleNoResources 43 | 44 | // PolicyView 45 | v.components.PolicyTable.Bind(v.Layout.Body) 46 | v.components.PolicyTable.Props.HandleNoResources = v.handleNoResources 47 | 48 | // PolicyAclView 49 | v.components.PolicyAclTable.Bind(v.Layout.Body) 50 | v.components.PolicyAclTable.Props.HandleNoResources = v.handleNoResources 51 | 52 | // SecretView 53 | v.components.SecretsTable.Bind(v.Layout.Body) 54 | v.components.SecretsTable.Props.HandleNoResources = v.handleNoResources 55 | 56 | // SecretObjectView 57 | v.components.SecretObjTable.Bind(v.Layout.Body) 58 | v.components.SecretObjTable.Props.HandleNoResources = v.handleNoResources 59 | 60 | // NamespaceTable 61 | v.components.NamespaceTable.Bind(v.Layout.Body) 62 | v.components.NamespaceTable.Props.HandleNoResources = v.handleNoResources 63 | 64 | // Selections 65 | //v.components.Selections.Bind(v.Layout.Elements.Dropdowns) 66 | // v.components.Selections.Render() 67 | //v.components.Selections.Init() 68 | 69 | // v.components.Selections.Namespace.SetSelectedFunc(func(option string, optionIndex int) { 70 | // v.Layout.Container.SetFocus(v.state.Elements.TableMain) 71 | // _, t := v.components.Selections.Namespace.Primitive().(*tview.DropDown).GetCurrentOption() 72 | // v.state.SelectedNamespace = t 73 | // v.logger.Debug().Msgf("New Namespace: %s", fmt.Sprintf("%s/%s", v.state.RootNamespace, v.state.SelectedNamespace)) 74 | // // Call the function to update the namespaces and the dropdown options 75 | // v.updateNamespaces() 76 | // v.UpdateVaultInfo() 77 | // }) 78 | 79 | // v.components.Selections.Props.DoneFunc = func(key tcell.Key) { 80 | 81 | // JumpToPolicy 82 | // v.components.JumpToPolicy.Bind(v.Layout.Footer) 83 | // v.components.JumpToPolicy.Props.DoneFunc = func(key tcell.Key) { 84 | // v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 0) 85 | // v.Layout.Footer.RemoveItem(v.components.JumpToPolicy.InputField.Primitive()) 86 | // v.Layout.Container.SetFocus(v.state.Elements.TableMain) 87 | 88 | // id := v.components.JumpToPolicy.InputField.GetText() 89 | // if id != "" { 90 | // //jobID := v.components.JumpToPolicy.InputField.GetText() 91 | // //v.Allocations(jobID) 92 | // } 93 | 94 | // v.components.JumpToPolicy.InputField.SetText("") 95 | // v.state.Toggle.JumpToPolicy = false 96 | // } 97 | 98 | // SearchField 99 | v.components.Search.Bind(v.Layout.Footer) 100 | v.components.Search.Props.DoneFunc = func(key tcell.Key) { 101 | v.state.Toggle.Search = false 102 | v.components.Search.InputField.SetText("") 103 | v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 0) 104 | v.Layout.Footer.RemoveItem(v.components.Search.InputField.Primitive()) 105 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 106 | v.components.TogglesInfo.Render() 107 | } 108 | 109 | v.components.Search.Props.ChangedFunc = func(text string) { 110 | v.FilterText = text 111 | } 112 | 113 | // TextInput (New secret) 114 | v.components.TextInfoInput.Bind(v.Layout.Footer) 115 | v.components.TextInfoInput.Props.DoneFunc = func(key tcell.Key) { 116 | v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 0) 117 | v.Layout.Footer.RemoveItem(v.components.TextInfoInput.InputField.Primitive()) 118 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 119 | v.components.TextInfoInput.Render() 120 | 121 | newText := v.components.TextInfoInput.InputField.GetText() 122 | v.state.NewSecretName = newText 123 | v.CreateNewSecretObject(newText) 124 | v.components.TextInfoInput.InputField.SetText("") 125 | v.state.Toggle.TextInput = false 126 | } 127 | 128 | // Error 129 | v.components.Error.Bind(v.Layout.Pages) 130 | v.components.Error.Props.Done = func(buttonIndex int, buttonLabel string) { 131 | if buttonLabel == "Quit" { 132 | v.Layout.Container.Stop() 133 | return 134 | } 135 | 136 | v.Layout.Pages.RemovePage(component.PageNameError) 137 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 138 | ///v.GoBack() 139 | } 140 | 141 | // Info 142 | v.components.Info.Bind(v.Layout.Pages) 143 | v.components.Info.Props.Done = func(buttonIndex int, buttonLabel string) { 144 | v.Layout.Pages.RemovePage(component.PageNameInfo) 145 | v.logger.Debug().Msgf("Info page removed, Active page is: %s", v.state.Elements.TableMain.GetTitle()) 146 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 147 | v.GoBack() 148 | } 149 | 150 | // Warn 151 | v.components.Failure.Bind(v.Layout.Pages) 152 | v.components.Failure.Props.Done = func(buttonIndex int, buttonLabel string) { 153 | v.Layout.Pages.RemovePage(component.PageNameInfo) 154 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 155 | v.GoBack() 156 | } 157 | 158 | v.Watcher.SubscribeHandler(models.HandleError, v.handleError) 159 | v.Watcher.SubscribeHandler(models.HandleFatal, v.handleFatal) 160 | 161 | stop := make(chan struct{}) 162 | 163 | go v.DrawLoop(stop) 164 | // v.logger.Debug().Msgf("Active page is: %s", v.state.Elements.TableMain.GetTitle()) 165 | // Set initial view to jobs 166 | v.Mounts() 167 | } 168 | 169 | func (v *View) UpdateVaultInfo() { 170 | // Update the component's state 171 | // newNS := fmt.Sprintf("%s/%s", v.state.RootNamespace, v.state.SelectedNamespace) 172 | 173 | v.components.VaultInfo.Props.Info = fmt.Sprintf( 174 | "%sAddress: %s%s %s\n%sVersion:%s %s\n", 175 | styles.HighlightSecondaryTag, 176 | styles.StandardColorTag, 177 | v.state.VaultAddress, 178 | styles.StandardColorTag, 179 | styles.HighlightSecondaryTag, 180 | styles.StandardColorTag, 181 | v.state.VaultVersion, 182 | ) 183 | 184 | // Re-render the component 185 | v.components.VaultInfo.Render() 186 | } 187 | 188 | // Function to update the namespaces and the dropdown options 189 | // func (v *View) updateNamespaces() { 190 | // var oldNamespace string 191 | // if v.state.SelectedNamespace == "" { 192 | // v.logger.Debug().Msgf("Changing namespace to: %s", fmt.Sprintf("%s/%s", v.state.RootNamespace, v.state.SelectedNamespace)) 193 | // v.state.Namespaces = v.Client.ChangeNamespace(v.state.RootNamespace) 194 | // v.logger.Debug().Msgf("New Namespaces: %s", v.state.Namespaces) 195 | // } else { 196 | // oldNamespace = v.state.SelectedNamespace 197 | // v.logger.Debug().Msgf("Changing namespace to: %s", fmt.Sprintf("%s/%s", v.state.RootNamespace, v.state.SelectedNamespace)) 198 | // v.state.Namespaces = v.Client.ChangeNamespace(fmt.Sprintf("%s/%s", v.state.RootNamespace, v.state.SelectedNamespace)) 199 | // v.logger.Debug().Msgf("New Namespaces: %s", v.state.Namespaces) 200 | // } 201 | // // Check if the namespaces slice is not empty 202 | // if len(v.state.Namespaces) > 0 { 203 | // v.components.Selections.Namespace.SetOptions(v.state.Namespaces, func(text string, index int) { 204 | // if v.state.SelectedNamespace == "" { 205 | // v.state.SelectedNamespace = text 206 | // } else { 207 | // v.state.SelectedNamespace = fmt.Sprintf("%s/%s", v.state.SelectedNamespace, text) 208 | // } 209 | // // Only update namespaces if the selected namespace has changed 210 | // if oldNamespace != v.state.SelectedNamespace { 211 | // // Use a separate goroutine for the recursive call 212 | // go v.updateNamespaces() 213 | // } 214 | // v.UpdateVaultInfo() 215 | // v.Draw() 216 | // }) 217 | // } else { 218 | // // Set a dummy option 219 | // v.components.Selections.Namespace.SetOptions([]string{"No namespaces available"}, nil) 220 | // } 221 | // } 222 | -------------------------------------------------------------------------------- /tui/view/inputs.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "github.com/gdamore/tcell/v2" 5 | ) 6 | 7 | // func (v *View) InputMounts(event *tcell.EventKey) *tcell.EventKey { 8 | // event = v.InputMainCommands(event) 9 | // return v.inputMounts(event) 10 | // } 11 | func (v *View) InputNamespaces(event *tcell.EventKey) *tcell.EventKey { 12 | event = v.InputMainCommands(event) 13 | return v.inputNamespaces(event) 14 | } 15 | 16 | func (v *View) InputMounts(event *tcell.EventKey) *tcell.EventKey { 17 | event = v.InputMainCommands(event) 18 | return v.inputMounts(event) 19 | } 20 | 21 | func (v *View) InputVaultPolicy(event *tcell.EventKey) *tcell.EventKey { 22 | event = v.InputMainCommands(event) 23 | return v.inputPolicy(event) 24 | } 25 | 26 | func (v *View) InputSecrets(event *tcell.EventKey) *tcell.EventKey { 27 | event = v.InputMainCommands(event) 28 | return v.inputSecrets(event) 29 | } 30 | 31 | func (v *View) InputSecret(event *tcell.EventKey) *tcell.EventKey { 32 | event = v.InputMainCommands(event) 33 | return v.inputSecret(event) 34 | } 35 | 36 | func (v *View) InputMainCommands(event *tcell.EventKey) *tcell.EventKey { 37 | if event == nil { 38 | return event 39 | } 40 | switch event.Key() { 41 | // Bug: CTRL+M key maps to enter and causes conflicts 42 | 43 | case tcell.KeyCtrlB: 44 | v.Watcher.Unsubscribe() 45 | v.Mounts() 46 | case tcell.KeyCtrlP: 47 | v.VPolicy() 48 | // Needs editing 49 | // case tcell.KeyCtrlJ: 50 | // v.SecretObject() 51 | case tcell.KeyCtrlT: 52 | v.Namespaces() 53 | case tcell.KeyRune: 54 | switch event.Rune() { 55 | 56 | case 's': 57 | if !v.Layout.Footer.HasFocus() { 58 | v.Layout.Container.SetFocus(v.state.Elements.DropDownNamespace) 59 | } 60 | } 61 | } 62 | 63 | return event 64 | } 65 | -------------------------------------------------------------------------------- /tui/view/mounts.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/internal/models" 5 | "github.com/dkyanakiev/vaulty/tui/component" 6 | "github.com/gdamore/tcell/v2" 7 | "github.com/rivo/tview" 8 | ) 9 | 10 | func (v *View) Mounts() { 11 | v.logger.Debug().Msg("Mounts view") 12 | v.viewSwitch() 13 | v.Layout.Body.SetTitle("Secret Mounts") 14 | v.Layout.Container.SetFocus(v.components.MountsTable.Table.Primitive()) 15 | v.Layout.Container.SetInputCapture(v.InputMounts) 16 | v.components.Commands.Update(component.MountsCommands) 17 | v.state.Elements.TableMain = v.components.MountsTable.Table.Primitive().(*tview.Table) 18 | v.components.MountsTable.Logger = v.logger 19 | v.components.SecretsTable.Props.SelectedPath = "" 20 | v.state.SelectedMount = "" 21 | v.state.SelectedPath = "" 22 | v.state.SelectedObject = "" 23 | 24 | update := func() { 25 | v.components.MountsTable.Props.Data = v.state.Mounts 26 | v.components.MountsTable.Render() 27 | v.Draw() 28 | v.logger.Debug().Msg("Updated mounts table") 29 | v.logger.Debug().Msgf("Selected mount is: %v", v.state.SelectedMount) 30 | v.logger.Debug().Msgf("Selected path is: %v", v.state.SelectedPath) 31 | } 32 | 33 | v.Watcher.SubscribeToMounts(update) 34 | // v.Watcher.UpdateMounts() 35 | update() 36 | 37 | // Add this view to the history 38 | // v.addToHistory(v.state.SelectedNamespace, "mounts", func() { 39 | // v.Mounts() 40 | // }) 41 | } 42 | 43 | func (v *View) parseMounts(data []*models.MountOutput) []*models.MountOutput { 44 | return nil 45 | } 46 | 47 | func (v *View) inputMounts(event *tcell.EventKey) *tcell.EventKey { 48 | if event == nil { 49 | return event 50 | } 51 | //todo 52 | switch event.Key() { 53 | case tcell.KeyEnter: 54 | if v.components.MountsTable.Table.Primitive().HasFocus() { 55 | v.state.SelectedMount = v.components.MountsTable.GetIDForSelection() 56 | v.Secrets("", "false") 57 | return nil 58 | } 59 | case tcell.KeyRune: 60 | switch event.Rune() { 61 | case 'e': 62 | if v.components.MountsTable.Table.Primitive().HasFocus() { 63 | v.state.SelectedMount = v.components.MountsTable.GetIDForSelection() 64 | v.Secrets("", "false") 65 | return nil 66 | } 67 | } 68 | } 69 | 70 | return event 71 | } 72 | -------------------------------------------------------------------------------- /tui/view/namespace.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/dkyanakiev/vaulty/tui/component" 7 | "github.com/gdamore/tcell/v2" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | func (v *View) Namespaces() { 12 | v.viewSwitch() 13 | v.logger.Debug().Msg("view: Namespaces") 14 | v.Layout.Body.SetTitle("Vault Nmespaces") 15 | v.Layout.Container.SetFocus(v.components.NamespaceTable.Table.Primitive()) 16 | 17 | v.state.Elements.TableMain = v.components.NamespaceTable.Table.Primitive().(*tview.Table) 18 | v.components.NamespaceTable.Logger = v.logger 19 | v.components.Commands.Update(component.NamespaceObjectCommands) 20 | v.Layout.Container.SetInputCapture(v.InputNamespaces) 21 | 22 | update := func() { 23 | // v.components.NamespaceTable.Props.Data = v.filterNamespaces(v.state.Namespaces) 24 | v.logger.Debug().Msgf("Current ns list: %v", v.state.Namespaces) 25 | v.components.NamespaceTable.Props.Data = v.state.Namespaces 26 | v.components.NamespaceTable.Render() 27 | v.Draw() 28 | v.components.NamespaceTable.Table.ScrollToTop() 29 | } 30 | 31 | v.components.Search.Props.ChangedFunc = func(text string) { 32 | //v.state.Filter.Namespace = text 33 | update() 34 | } 35 | 36 | v.Watcher.SubscribeToNamespaces(update) 37 | 38 | update() 39 | 40 | // v.components.Selections.Namespace.SetSelectedFunc(func(text string, index int) { 41 | // v.state.SelectedNamespace = text 42 | // v.Namespaces() 43 | // }) 44 | 45 | // v.addToHistory(v.state.SelectedNamespace, models.TopicNamespace, v.Namespaces) 46 | } 47 | 48 | func (v *View) inputNamespaces(event *tcell.EventKey) *tcell.EventKey { 49 | if event == nil { 50 | return event 51 | } 52 | 53 | switch event.Key() { 54 | case tcell.KeyEsc: 55 | //v.GoBack() 56 | case tcell.KeyCtrlD: 57 | v.logger.Debug().Msgf("Going back to default namespace: %v", v.state.DefaultNamespace) 58 | v.state.SelectedNamespace = v.state.DefaultNamespace 59 | v.components.TogglesInfo.Props.Namespace = v.state.DefaultNamespace 60 | v.components.TogglesInfo.Render() 61 | v.state.SelectedNamespace = v.state.DefaultNamespace 62 | //v.Client.ChangeNamespace(v.state.DefaultNamespace) 63 | v.Watcher.Unsubscribe() 64 | v.Mounts() 65 | return nil 66 | case tcell.KeyCtrlW: 67 | v.logger.Debug().Msgf("Going back to root namespace : %v", v.state.RootNamespace) 68 | v.state.SelectedNamespace = v.state.RootNamespace 69 | v.components.TogglesInfo.Props.Namespace = v.state.RootNamespace 70 | v.components.TogglesInfo.Render() 71 | v.state.SelectedNamespace = v.state.RootNamespace 72 | // v.Client.ChangeNamespace(v.state.RootNamespace) 73 | v.Watcher.Unsubscribe() 74 | v.Mounts() 75 | return nil 76 | case tcell.KeyEnter: 77 | selectdNs := v.components.NamespaceTable.GetIDForSelection() 78 | v.logger.Debug().Msgf("Selected namespace is: %v", selectdNs) 79 | newNs := fmt.Sprintf("%s/%s", v.state.SelectedNamespace, selectdNs) 80 | v.logger.Debug().Msgf("Changing namespace to: %s", newNs) 81 | v.components.TogglesInfo.Props.Namespace = newNs 82 | v.components.TogglesInfo.Render() 83 | v.state.SelectedNamespace = newNs 84 | // v.Client.ChangeNamespace(newNs) 85 | v.Watcher.Unsubscribe() 86 | v.Mounts() 87 | return nil 88 | } 89 | 90 | return event 91 | } 92 | 93 | func getNamespaceNameIndex(name string, ns []string) int { 94 | var index int 95 | for i, n := range ns { 96 | if n == name { 97 | index = i 98 | } 99 | } 100 | 101 | return index 102 | } 103 | 104 | // func (v *View) filterNamespaces(data []*models.Namespace) []*models.Namespace { 105 | // filter := v.state.Filter.Namespace 106 | // if filter != "" { 107 | // rx, _ := regexp.Compile(filter) 108 | // result := []*models.Namespace{} 109 | // for _, ns := range v.state.Namespaces { 110 | // switch true { 111 | // case rx.MatchString(ns.Name), 112 | // rx.MatchString(ns.Description): 113 | // result = append(result, ns) 114 | // } 115 | // } 116 | 117 | // return result 118 | // } 119 | 120 | // return data 121 | // } 122 | -------------------------------------------------------------------------------- /tui/view/policy.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "regexp" 5 | 6 | "github.com/dkyanakiev/vaulty/tui/component" 7 | "github.com/gdamore/tcell/v2" 8 | "github.com/rivo/tview" 9 | ) 10 | 11 | func (v *View) VPolicy() { 12 | v.viewSwitch() 13 | v.Layout.Body.Clear() 14 | v.Layout.Body.SetTitle("Vault Policies") 15 | v.Layout.Container.SetFocus(v.components.PolicyTable.Table.Primitive()) 16 | v.Layout.Container.SetInputCapture(v.InputVaultPolicy) 17 | v.components.Commands.Update(component.PolicyCommands) 18 | search := v.components.Search 19 | //table := v.components.in 20 | 21 | update := func() { 22 | if v.state.Toggle.Search { 23 | v.state.Filter.Policy = v.FilterText 24 | } 25 | v.components.PolicyTable.Props.Data = v.filterPolicies() 26 | v.components.PolicyTable.Render() 27 | v.Draw() 28 | v.components.PolicyTable.Table.ScrollToTop() 29 | } 30 | 31 | search.Props.ChangedFunc = func(text string) { 32 | v.FilterText = text 33 | update() 34 | } 35 | 36 | v.Watcher.SubscribeToPolicies(update) 37 | update() 38 | 39 | v.state.Elements.TableMain = v.components.PolicyTable.Table.Primitive().(*tview.Table) 40 | } 41 | 42 | func (v *View) inputPolicy(event *tcell.EventKey) *tcell.EventKey { 43 | if event == nil { 44 | return event 45 | } 46 | 47 | switch event.Key() { 48 | case tcell.KeyEsc: 49 | //v.GoBack() 50 | case tcell.KeyEnter: 51 | if v.components.PolicyTable.Table.Primitive().HasFocus() { 52 | v.PolicyACL(v.components.PolicyTable.GetIDForSelection()) 53 | v.Watcher.Unsubscribe() 54 | return nil 55 | } 56 | case tcell.KeyRune: 57 | switch event.Rune() { 58 | case '/': 59 | if !v.Layout.Footer.HasFocus() { 60 | if !v.state.Toggle.Search { 61 | v.state.Toggle.Search = true 62 | v.components.Search.InputField.SetText("") 63 | v.Search() 64 | } else { 65 | v.Layout.Container.SetFocus(v.components.Search.InputField.Primitive()) 66 | } 67 | return nil 68 | } 69 | case 'i': 70 | if v.components.PolicyTable.Table.Primitive().HasFocus() { 71 | v.PolicyACL(v.components.PolicyTable.GetIDForSelection()) 72 | v.Watcher.Unsubscribe() 73 | return nil 74 | } 75 | } 76 | 77 | } 78 | 79 | return event 80 | } 81 | 82 | func (v *View) filterPolicies() []string { 83 | data := v.state.PolicyList 84 | filter := v.state.Filter.Policy 85 | if filter != "" { 86 | rx, _ := regexp.Compile(filter) 87 | result := []string{} 88 | for _, p := range data { 89 | switch true { 90 | case rx.MatchString(p): 91 | result = append(result, p) 92 | } 93 | } 94 | return result 95 | } 96 | return data 97 | } 98 | -------------------------------------------------------------------------------- /tui/view/policyacl.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "github.com/dkyanakiev/vaulty/tui/component" 5 | "github.com/gdamore/tcell/v2" 6 | "github.com/rivo/tview" 7 | ) 8 | 9 | func (v *View) PolicyACL(policyName string) { 10 | 11 | v.viewSwitch() 12 | v.Layout.Body.SetTitle(policyName) 13 | v.Layout.Container.SetFocus(v.components.PolicyAclTable.TextView.Primitive()) 14 | v.components.PolicyAclTable.TextView.Clear().ScrollToBeginning() 15 | v.components.Commands.Update(component.PolicyACLCommands) 16 | v.Layout.Container.SetInputCapture(v.inputPolicyACL) 17 | 18 | v.state.SelectedPolicyName = policyName 19 | v.components.PolicyAclTable.Props.SelectedPolicyName = policyName 20 | 21 | update := func() { 22 | v.components.PolicyAclTable.Props.SelectedPolicyACL = v.state.PolicyACL 23 | v.components.PolicyAclTable.Render() 24 | v.Draw() 25 | } 26 | v.Watcher.SubscribeToPoliciesACL(update) 27 | update() 28 | 29 | v.state.Elements.TextMain = v.components.PolicyAclTable.TextView.Primitive().(*tview.TextView) 30 | 31 | } 32 | 33 | func (v *View) inputPolicyACL(event *tcell.EventKey) *tcell.EventKey { 34 | if event == nil { 35 | return event 36 | } 37 | 38 | switch event.Key() { 39 | case tcell.KeyEsc: 40 | v.VPolicy() 41 | } 42 | return event 43 | } 44 | -------------------------------------------------------------------------------- /tui/view/secretobj.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/atotto/clipboard" 7 | "github.com/dkyanakiev/vaulty/tui/component" 8 | "github.com/gdamore/tcell/v2" 9 | "github.com/rivo/tview" 10 | ) 11 | 12 | func (v *View) SecretObject(mount, path string) { 13 | v.viewSwitch() 14 | v.Layout.Body.SetTitle("Secret object") 15 | v.Layout.Container.SetInputCapture(v.InputSecret) 16 | v.Layout.Container.SetFocus(v.components.SecretObjTable.Table.Primitive()) 17 | v.components.Commands.Update(component.SecretObjectCommands) 18 | 19 | v.logger.Debug().Msgf("Selected mount is: %v", mount) 20 | v.logger.Debug().Msgf("Selected path is: %v", path) 21 | v.state.Elements.TableMain = v.components.SecretObjTable.Table.Primitive().(*tview.Table) 22 | v.components.SecretObjTable.Logger = v.logger 23 | v.components.SecretObjTable.Props.SelectedPath = path 24 | v.components.SecretObjTable.Props.ObscureSecrets = true 25 | 26 | update := func() { 27 | v.logger.Debug().Msgf("Focus set to %s", v.state.Elements.TableMain.GetTitle()) 28 | v.logger.Debug().Msgf("Selected path is: %v", v.state.SelectedPath) 29 | if !v.components.SecretObjTable.Editable { 30 | v.components.SecretObjTable.Render() 31 | v.components.SecretObjTable.Props.Data = v.state.SelectedSecret 32 | v.components.SecretObjTable.Props.Metadata = v.state.SelectedSecretMeta 33 | v.Draw() 34 | } 35 | } 36 | 37 | v.Watcher.SubscribeToSecret(mount, path, update) 38 | update() 39 | 40 | v.state.Elements.TableMain = v.components.SecretObjTable.Table.Primitive().(*tview.Table) 41 | 42 | } 43 | 44 | func (v *View) inputSecret(event *tcell.EventKey) *tcell.EventKey { 45 | if event == nil { 46 | return event 47 | } 48 | 49 | switch event.Key() { 50 | case tcell.KeyRune: 51 | if !v.components.SecretObjTable.Editable { 52 | switch event.Rune() { 53 | case 'h': 54 | v.components.SecretObjTable.Props.ObscureSecrets = !v.components.SecretObjTable.Props.ObscureSecrets 55 | v.components.SecretObjTable.Render() 56 | return nil 57 | case 'c': 58 | if v.components.SecretObjTable.ShowJson { 59 | content := v.components.SecretObjTable.TextView.GetText(true) 60 | clipboard.WriteAll(content) 61 | } else { 62 | row, _ := v.components.SecretObjTable.Table.GetSelection() 63 | if row > 0 { // Ignore the header row 64 | // Get the content of the row 65 | content := v.components.SecretObjTable.Table.GetCellContent(row, 1) 66 | // Copy the content to the clipboard 67 | clipboard.WriteAll(content) 68 | } 69 | } 70 | return nil 71 | case 'b': 72 | v.goBack() 73 | case 't': 74 | v.components.SecretObjTable.ShowMetadata = !v.components.SecretObjTable.ShowMetadata 75 | v.logger.Debug().Msgf("Show metadata: %v", v.state.SelectedSecretMeta.UpdatedTime) 76 | v.components.SecretObjTable.ToggleMetaView() 77 | case 'j': 78 | v.components.SecretObjTable.ShowJson = !v.components.SecretObjTable.ShowJson 79 | v.components.SecretObjTable.ToggleView() 80 | case 'P': 81 | v.components.Commands.Update(component.SecretsObjectPatchCommands) 82 | v.components.SecretObjTable.Editable = true 83 | v.components.TogglesInfo.Props.Editable = true 84 | v.components.SecretObjTable.Props.Update = "PATCH" 85 | v.components.TogglesInfo.Render() 86 | v.components.SecretObjTable.ToggleView() 87 | v.components.SecretObjTable.TextView.ScrollToBeginning() 88 | v.Layout.Container.SetFocus(v.components.SecretObjTable.TextArea.Primitive()) 89 | return nil 90 | case 'U': 91 | v.components.Commands.Update(component.SecretsObjectPatchCommands) 92 | v.components.SecretObjTable.Editable = true 93 | v.components.TogglesInfo.Props.Editable = true 94 | v.components.SecretObjTable.Props.Update = "UPDATE" 95 | v.components.TogglesInfo.Render() 96 | v.components.SecretObjTable.ToggleView() 97 | v.components.SecretObjTable.TextView.ScrollToBeginning() 98 | v.Layout.Container.SetFocus(v.components.SecretObjTable.TextArea.Primitive()) 99 | return nil 100 | } 101 | } 102 | case tcell.KeyCtrlW: 103 | v.patchSecret() 104 | v.goBack() 105 | return nil 106 | case tcell.KeyEsc: 107 | if v.components.SecretObjTable.Editable { 108 | v.components.SecretObjTable.Editable = false 109 | v.components.TogglesInfo.Props.Editable = false 110 | v.components.TogglesInfo.Render() 111 | v.components.SecretObjTable.ToggleView() 112 | v.Layout.Container.SetFocus(v.components.SecretObjTable.Table.Primitive()) 113 | } else { 114 | v.goBack() 115 | } 116 | 117 | } 118 | 119 | return event 120 | } 121 | 122 | func (v *View) goBack() { 123 | v.state.SelectedPath = strings.TrimSuffix(v.state.SelectedPath, "/") // Remove trailing slash 124 | lastSlashIndex := strings.LastIndex(v.state.SelectedPath, "/") 125 | if lastSlashIndex != -1 { 126 | v.state.SelectedPath = v.state.SelectedPath[:lastSlashIndex+1] // Keep the slash 127 | } else if v.state.SelectedPath != "" { 128 | v.state.SelectedPath = "" // If no slash left and it's not empty, set to empty 129 | v.components.SecretsTable.Props.SelectedPath = "" 130 | } 131 | v.Secrets(v.state.SelectedPath, "false") 132 | } 133 | 134 | func (v *View) patchSecret() { 135 | var patch bool 136 | if v.components.SecretObjTable.Props.Update == "PATCH" { 137 | v.logger.Debug().Msg("PATCH secret") 138 | patch = true 139 | } else { 140 | v.logger.Debug().Msg("UPDATE secret") 141 | patch = false 142 | } 143 | ok := v.components.SecretObjTable.SaveData(v.components.SecretObjTable.TextArea.GetText()) 144 | if ok != "" { 145 | v.handleError(ok) 146 | } else { 147 | if v.state.Enterprise { 148 | v.logger.Debug().Msgf("Enterprise version detected, setting namespace to %v", v.state.SelectedNamespace) 149 | v.Client.ChangeNamespace(v.state.SelectedNamespace) 150 | } 151 | v.logger.Debug().Msgf("Updated secret object is: %v", v.components.SecretObjTable.Props.UpdatedData) 152 | err := v.Client.UpdateSecretObjectKV2(v.state.SelectedMount, v.components.SecretObjTable.Props.SelectedPath, patch, v.components.SecretObjTable.Props.UpdatedData) 153 | 154 | if err != nil { 155 | v.handleError(string(err.Error())) 156 | } 157 | v.components.SecretObjTable.Editable = false 158 | v.components.SecretObjTable.ToggleView() 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /tui/view/secrets.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/dkyanakiev/vaulty/internal/models" 10 | "github.com/dkyanakiev/vaulty/tui/component" 11 | "github.com/gdamore/tcell/v2" 12 | "github.com/rivo/tview" 13 | ) 14 | 15 | // base path - v.state.SelectedMount 16 | // latest path - path 17 | // full path - path + path 18 | 19 | func (v *View) Secrets(path string, secretBool string) { 20 | v.viewSwitch() 21 | v.Layout.Body.Clear() 22 | v.Layout.Body.SetTitle(fmt.Sprintf("Secrets: %s", path)) 23 | v.Layout.Container.SetFocus(v.components.SecretsTable.Table.Primitive()) 24 | v.Layout.Container.SetInputCapture(v.InputSecrets) 25 | v.components.Commands.Update(component.SecretsCommands) 26 | v.logger.Debug().Msgf("Selected path for secret is: %v", path) 27 | 28 | search := v.components.Search 29 | v.state.Toggle.Search = false 30 | v.state.Filter.Object = "" 31 | 32 | v.components.SecretsTable.Props.SelectedMount = v.state.SelectedMount 33 | if path != "" { 34 | v.components.SecretsTable.Props.SelectedPath = fmt.Sprintf("%s%s", v.state.SelectedPath, v.state.SelectedObject) 35 | } 36 | 37 | update := func() { 38 | if v.state.Toggle.Search { 39 | v.state.Filter.Object = v.FilterText 40 | v.components.TogglesInfo.Props.FilterText = v.FilterText 41 | } 42 | v.components.SecretsTable.Props.Data = v.filterSecrets() 43 | v.components.SecretsTable.Props.SelectedMount = v.state.SelectedMount 44 | 45 | v.components.SecretsTable.Render() 46 | v.Draw() 47 | v.components.SecretsTable.Table.ScrollToTop() 48 | } 49 | 50 | search.Props.ChangedFunc = func(text string) { 51 | v.FilterText = text 52 | update() 53 | } 54 | 55 | v.Watcher.SubscribeToSecrets(v.components.SecretsTable.Props.SelectedMount, 56 | v.components.SecretsTable.Props.SelectedPath, update) 57 | update() 58 | 59 | v.state.Elements.TableMain = v.components.SecretsTable.Table.Primitive().(*tview.Table) 60 | 61 | } 62 | 63 | func (v *View) inputSecrets(event *tcell.EventKey) *tcell.EventKey { 64 | if event == nil { 65 | return event 66 | } 67 | 68 | switch event.Key() { 69 | case tcell.KeyEsc: 70 | if v.components.SecretsTable.Table.Primitive().HasFocus() { 71 | v.state.SelectedPath = strings.TrimSuffix(v.state.SelectedPath, "/") // Remove trailing slash 72 | lastSlashIndex := strings.LastIndex(v.state.SelectedPath, "/") 73 | if lastSlashIndex != -1 { 74 | v.state.SelectedPath = v.state.SelectedPath[:lastSlashIndex+1] // Keep the slash 75 | } else if v.state.SelectedPath != "" { 76 | v.state.SelectedPath = "" // If no slash left and it's not empty, set to empty 77 | v.components.SecretsTable.Props.SelectedPath = "" 78 | } 79 | v.Secrets(v.state.SelectedPath, "false") 80 | } 81 | case tcell.KeyEnter: 82 | if v.components.SecretsTable.Table.Primitive().HasFocus() { 83 | path, secretBool := v.components.SecretsTable.GetIDForSelection() 84 | v.state.SelectedPath = fmt.Sprintf("%s%s", v.state.SelectedPath, path) 85 | if secretBool == "true" { 86 | v.SecretObject(v.state.SelectedMount, v.state.SelectedPath) 87 | } else { 88 | v.logger.Debug().Msgf("Running Secrets view with : %v", path) 89 | v.Secrets(path, secretBool) 90 | } 91 | return nil 92 | } 93 | case tcell.KeyCtrlN: 94 | v.logger.Debug().Msgf("Running New Secret view with : %v", v.state.SelectedPath) 95 | if !v.Layout.Footer.HasFocus() { 96 | if !v.state.Toggle.TextInput { 97 | v.state.Toggle.TextInput = true 98 | v.components.TextInfoInput.InputField.SetText("") 99 | v.TextInput() 100 | } else { 101 | v.Layout.Container.SetFocus(v.components.TextInfoInput.InputField.Primitive()) 102 | } 103 | return nil 104 | } 105 | case tcell.KeyRune: 106 | switch event.Rune() { 107 | case 'e': 108 | if v.components.SecretsTable.Table.Primitive().HasFocus() { 109 | path, secretBool := v.components.SecretsTable.GetIDForSelection() 110 | v.state.SelectedPath = fmt.Sprintf("%s%s", v.state.SelectedPath, path) 111 | if secretBool == "true" { 112 | v.SecretObject(v.state.SelectedMount, v.state.SelectedPath) 113 | } else { 114 | v.Secrets(path, secretBool) 115 | } 116 | return nil 117 | } 118 | 119 | //TODO: Need to clean this up 120 | case 'b': 121 | if v.components.SecretsTable.Table.Primitive().HasFocus() { 122 | v.state.SelectedPath = strings.TrimSuffix(v.state.SelectedPath, "/") // Remove trailing slash 123 | lastSlashIndex := strings.LastIndex(v.state.SelectedPath, "/") 124 | if lastSlashIndex != -1 { 125 | v.state.SelectedPath = v.state.SelectedPath[:lastSlashIndex+1] // Keep the slash 126 | } else if v.state.SelectedPath != "" { 127 | v.state.SelectedPath = "" // If no slash left and it's not empty, set to empty 128 | v.components.SecretsTable.Props.SelectedPath = "" 129 | } 130 | v.Secrets(v.state.SelectedPath, "false") 131 | } 132 | case '/': 133 | if !v.Layout.Footer.HasFocus() { 134 | if !v.state.Toggle.Search { 135 | v.state.Toggle.Search = true 136 | v.components.Search.InputField.SetText("") 137 | v.Search() 138 | } else { 139 | v.Layout.Container.SetFocus(v.components.Search.InputField.Primitive()) 140 | } 141 | return nil 142 | } 143 | } 144 | } 145 | 146 | return event 147 | } 148 | 149 | func (v *View) filterSecrets() []models.SecretPath { 150 | data := v.state.SecretsData 151 | filter := v.state.Filter.Object 152 | if filter != "" { 153 | rx, _ := regexp.Compile(filter) 154 | var result []models.SecretPath 155 | for _, p := range data { 156 | switch true { 157 | case rx.MatchString(p.PathName): 158 | result = append(result, p) 159 | } 160 | } 161 | return result 162 | } 163 | 164 | return data 165 | } 166 | 167 | func trimLastElement(s string) string { 168 | dir, _ := filepath.Split(s) 169 | return strings.TrimSuffix(dir, string(filepath.Separator)) + string(filepath.Separator) 170 | } 171 | 172 | func (v *View) CreateNewSecretObject(newObj string) { 173 | v.logger.Info().Msgf("Creating new secret object for path: %v", v.components.SecretsTable.Props.SelectedPath) 174 | v.logger.Info().Msgf("Creating new secret object for mount: %v", v.state.SelectedMount) 175 | if v.state.NewSecretName != "" { 176 | v.logger.Debug().Msgf("New secret name is: %v", v.state.NewSecretName) 177 | newPath := fmt.Sprintf("%s%s", v.components.SecretsTable.Props.SelectedPath, v.state.NewSecretName) 178 | err := v.Client.CreateNewSecret(v.state.SelectedMount, newPath) 179 | if err != nil { 180 | v.handleError(string(err.Error())) 181 | } else { 182 | v.handleInfo("Secret path created successfully") 183 | } 184 | 185 | } else { 186 | v.logger.Debug().Msg("New secret name is empty") 187 | } 188 | //v.Layout.Container.SetFocus(v.components.SecretObjTable.Table.Primitive()) 189 | } 190 | -------------------------------------------------------------------------------- /tui/view/view.go: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/dkyanakiev/vaulty/internal/models" 7 | "github.com/dkyanakiev/vaulty/internal/state" 8 | "github.com/dkyanakiev/vaulty/tui/component" 9 | "github.com/dkyanakiev/vaulty/tui/layout" 10 | "github.com/rs/zerolog" 11 | ) 12 | 13 | const ( 14 | historySize = 15 15 | ) 16 | 17 | type Client interface { 18 | UpdateSecretObjectKV2(mount string, path string, update bool, data map[string]interface{}) error 19 | CreateNewSecret(mount string, path string) error 20 | ListNamespaces() ([]string, error) 21 | ChangeNamespace(ns string) []string 22 | } 23 | 24 | type Watcher interface { 25 | Subscribe(notify func(), topics ...string) 26 | Unsubscribe() 27 | SubscribeHandler(handler models.Handler, handle func(string, ...interface{})) 28 | SubscribeToPolicies(notify func()) 29 | SubscribeToPoliciesACL(notify func()) 30 | SubscribeToMounts(notify func()) 31 | SubscribeToNamespaces(notify func()) 32 | SubscribeToSecrets(selectedMount, selectedPath string, notify func()) 33 | SubscribeToSecret(selectedMount, selectedPath string, notify func()) 34 | UpdateMounts() 35 | } 36 | 37 | type View struct { 38 | Client Client 39 | Watcher Watcher 40 | Layout *layout.Layout 41 | 42 | history *History 43 | state *state.State 44 | logger *zerolog.Logger 45 | components *Components 46 | mutex sync.Mutex 47 | 48 | FilterText string "" 49 | 50 | draw chan struct{} 51 | } 52 | 53 | type Components struct { 54 | MountsTable *component.MountsTable 55 | PolicyTable *component.PolicyTable 56 | PolicyAclTable *component.PolicyAclTable 57 | SecretsTable *component.SecretsTable 58 | SecretObjTable *component.SecretObjTable 59 | NamespaceTable *component.NamespaceTable 60 | Commands *component.Commands 61 | VaultInfo *component.VaultInfo 62 | Search *component.SearchField 63 | Error *component.Error 64 | Info *component.Info 65 | Failure *component.Info 66 | TogglesInfo *component.TogglesInfo 67 | Selections *component.Selections 68 | JumpToPolicy *component.JumpToPolicy 69 | TextInfoInput *component.TextInfoInput 70 | Logo *component.Logo 71 | Logger *zerolog.Logger 72 | } 73 | 74 | func New(components *Components, watcher Watcher, client Client, state *state.State, logger *zerolog.Logger) *View { 75 | components.Search = component.NewSearchField("") 76 | components.TextInfoInput = component.NewTextInfoInput() 77 | 78 | return &View{ 79 | Client: client, 80 | Watcher: watcher, 81 | state: state, 82 | Layout: layout.New(layout.Default, layout.EnableMouse), 83 | draw: make(chan struct{}, 1), 84 | logger: logger, 85 | components: components, 86 | history: &History{ 87 | HistorySize: historySize, 88 | Logger: logger, 89 | }, 90 | } 91 | } 92 | 93 | func (v *View) Draw() { 94 | v.draw <- struct{}{} 95 | } 96 | 97 | // DrawLoop refreshes the screen when it receives a 98 | // signal on the draw channel. This function should 99 | // be run inside a goroutine as tview.Application.Draw() 100 | // can deadlock when called from the main thread. 101 | func (v *View) DrawLoop(stop chan struct{}) { 102 | for { 103 | select { 104 | case <-v.draw: 105 | v.Layout.Container.Draw() 106 | case <-stop: 107 | return 108 | } 109 | 110 | } 111 | } 112 | 113 | func (v *View) GoBack() { 114 | v.history.pop() 115 | } 116 | 117 | func (v *View) addToHistory(ns string, topic string, update func()) { 118 | v.history.push(func() { 119 | v.state.SelectedNamespace = ns 120 | // update() 121 | 122 | // v.components.Selections.Props.Rerender = update 123 | v.components.Selections.Namespace.SetSelectedFunc(func(text string, index int) { 124 | v.state.SelectedNamespace = text 125 | update() 126 | }) 127 | // v.Watcher.Subscribe(topic, update) 128 | 129 | index := getNamespaceNameIndex(ns, v.state.Namespaces) 130 | v.state.Elements.DropDownNamespace.SetCurrentOption(index) 131 | }) 132 | } 133 | 134 | func (v *View) viewSwitch() { 135 | v.resetSearch() 136 | } 137 | 138 | func (v *View) Search() { 139 | search := v.components.Search 140 | v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 1) 141 | search.Render() 142 | v.Layout.Container.SetFocus(search.InputField.Primitive()) 143 | } 144 | 145 | func (v *View) resetSearch() { 146 | if v.state.Toggle.Search { 147 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 148 | v.Layout.Footer.RemoveItem(v.components.Search.InputField.Primitive()) 149 | v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 0) 150 | v.state.Toggle.Search = false 151 | } 152 | } 153 | 154 | func (v *View) TextInput() { 155 | textIn := v.components.TextInfoInput 156 | v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 1) 157 | textIn.Render() 158 | v.Layout.Container.SetFocus(textIn.InputField.Primitive()) 159 | } 160 | 161 | func (v *View) resetTextInput() { 162 | if v.state.Toggle.TextInput { 163 | v.Layout.Container.SetFocus(v.state.Elements.TableMain) 164 | v.Layout.Footer.RemoveItem(v.components.TextInfoInput.InputField.Primitive()) 165 | v.Layout.MainPage.ResizeItem(v.Layout.Footer, 0, 0) 166 | v.state.Toggle.TextInput = false 167 | } 168 | } 169 | 170 | // func (v *View) addToHistory(ns string, topic api.Topic, update func()) { 171 | // v.history.push(func() { 172 | // //v.state.SelectedNamespace = ns 173 | // // update() 174 | 175 | // // v.components.Selections.Props.Rerender = update 176 | // v.components.Selections.Namespace.SetSelectedFunc(func(text string, index int) { 177 | // v.state.SelectedNamespace = text 178 | // update() 179 | // }) 180 | // // v.Watcher.Subscribe(topic, update) 181 | 182 | // index := getNamespaceNameIndex(ns, v.state.Namespaces) 183 | // v.state.Elements.DropDownNamespace.SetCurrentOption(index) 184 | // }) 185 | // } 186 | --------------------------------------------------------------------------------