feat(vault-tui): implement KV client and service for managing secrets

- Added internal/vault/client.go for creating a Vault client with configuration settings.
- Introduced internal/vault/errors.go to classify Vault API errors for better UI handling.
- Created internal/vault/kv.go to manage KV secrets, including listing, reading, writing, and deleting operations.
- Implemented internal/vault/mounts.go to list and describe secret engine mounts.
- Developed internal/vault/service.go to provide a unified entry point for Vault operations.
- Added internal/vault/kv_test.go for comprehensive testing of KV operations.
- Introduced internal/ui/toast.go for transient notifications in the UI.
- Added renovate.json for dependency management and updates.
This commit is contained in:
2026-08-14 11:09:03 +02:00
commit ae30ba1240
85 changed files with 9413 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
package cli
import (
"bufio"
"fmt"
"os"
"strings"
"git.morlana.online/f.weber/vault-tui/internal/auth"
"git.morlana.online/f.weber/vault-tui/internal/config"
)
// runWizard interactively builds one profile and writes it to path. It is
// deliberately generic — it prompts for everything (address, namespace,
// TLS, auth method and that method's fields) rather than assuming any
// particular Vault deployment, per this project's design goal of not
// hard-coding any environment's specifics into the tool itself.
func runWizard(path string) (*config.File, error) {
r := bufio.NewReader(os.Stdin)
fmt.Fprintln(os.Stderr, "No config found — let's set up a Vault profile (you can edit it later at "+path+").")
profileName := ask(r, "Profile name", "default")
addrDefault := "https://127.0.0.1:8200"
if v, ok := config.OSEnviron(config.EnvAddress); ok && v != "" {
addrDefault = v
}
address := ask(r, "Vault address", addrDefault)
namespace := ask(r, "Namespace (blank for none)", "")
prof := &config.Profile{Address: address, Namespace: namespace}
if askYesNo(r, "Configure custom TLS (CA cert / client cert)?", false) {
prof.TLS.CACert = ask(r, "CA certificate path (blank to use system trust)", "")
prof.TLS.ClientCert = ask(r, "Client certificate path (blank for none)", "")
if prof.TLS.ClientCert != "" {
prof.TLS.ClientKey = ask(r, "Client key path", "")
}
if askYesNo(r, "Skip TLS verification (insecure, testing only)?", false) {
t := true
prof.TLS.SkipVerify = &t
}
}
names := auth.Default().Names()
fmt.Fprintln(os.Stderr, "Available auth methods: "+strings.Join(names, ", "))
method := ask(r, "Auth method", "oidc")
m, ok := auth.Default().Get(method)
if !ok {
return nil, fmt.Errorf("unknown auth method %q", method)
}
prof.Auth.Method = method
mount := ask(r, "Auth mount path", m.DefaultMount())
prof.Auth.Mount = mount
params := map[string]string{}
for _, f := range m.Fields() {
if f.Kind == auth.FieldSecret {
// Secrets are never written to the config file; the user supplies
// them at login time (env var, flag, or interactive prompt).
continue
}
v := ask(r, f.Label, f.Default)
if v != "" {
params[f.Name] = v
}
}
if len(params) > 0 {
prof.Auth.Params = params
}
file := &config.File{
Version: config.SchemaVersion,
CurrentProfile: profileName,
Profiles: map[string]*config.Profile{profileName: prof},
}
if err := config.Save(path, file); err != nil {
return nil, err
}
fmt.Fprintln(os.Stderr, "Saved "+path)
return file, nil
}
func ask(r *bufio.Reader, label, def string) string {
if def != "" {
fmt.Fprintf(os.Stderr, "%s [%s]: ", label, def)
} else {
fmt.Fprintf(os.Stderr, "%s: ", label)
}
line, _ := r.ReadString('\n')
line = strings.TrimSpace(line)
if line == "" {
return def
}
return line
}
func askYesNo(r *bufio.Reader, label string, def bool) bool {
d := "y/N"
if def {
d = "Y/n"
}
line := strings.ToLower(ask(r, label+" ("+d+")", ""))
if line == "" {
return def
}
return line == "y" || line == "yes"
}