feat: lazysecrets list shows unreadable lists as red ✗ lines, and filters by kind
This commit is contained in:
6 files changed
+197
-81
No files matched your search
+36
-23
@@ -9,6 +9,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -32,26 +33,33 @@ import (
|
||||
// ui.icons); the link never is.
|
||||
//
|
||||
// What's cached prints at once; what's missing or older than --max-age
|
||||
// follows as it loads. Lists that can't be read are reported to errOut:
|
||||
// stderr when the output is on the terminal, nothing when it goes into
|
||||
// fzf, whose screen they'd draw over; --errors reports them to stderr
|
||||
// either way.
|
||||
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons bool, args []string, out, errOut, stderr io.Writer) error {
|
||||
// follows as it loads. A list that can't be read is a line too, as it
|
||||
// fails: ✗, the vault or source, and why, in the theme's error colour.
|
||||
// They're in the output rather than on stderr, which would draw over fzf.
|
||||
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons bool, args []string, out io.Writer) error {
|
||||
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||
cached := fs.Bool("cached", false, "only what's cached, and repos: load nothing from Azure or Databricks")
|
||||
maxAge := fs.Duration("max-age", 2*time.Minute, "load cached lists older than this again")
|
||||
format := fs.String("format", "text", "text: name and link, tab-separated; json: an object per line")
|
||||
color := fs.String("color", "always", "colour the names (text): always or never; fzf needs --ansi")
|
||||
iconsOn := fs.Bool("icons", withIcons, "put the provider's Nerd Font icon before names (text)")
|
||||
showErrors := fs.Bool("errors", false, "report lists that can't be read on stderr also when the output goes into a pipe")
|
||||
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
||||
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *showErrors {
|
||||
errOut = stderr
|
||||
}
|
||||
if fs.NArg() > 0 {
|
||||
return fmt.Errorf("usage: lazysecrets list [--cached] [--max-age 2m] [--format text|json]")
|
||||
return fmt.Errorf("usage: lazysecrets list [--kind secret,…] [--cached] [--max-age 2m] [--format text|json]")
|
||||
}
|
||||
var only []string
|
||||
for k := range strings.SplitSeq(*kinds, ",") {
|
||||
if k = strings.TrimSpace(k); k == "" {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(listing.Kinds, k) {
|
||||
return fmt.Errorf("--kind: %q is not one of %s", k, strings.Join(listing.Kinds, ", "))
|
||||
}
|
||||
only = append(only, k)
|
||||
}
|
||||
if *color != "always" && *color != "never" {
|
||||
return fmt.Errorf("--color: %q is not always or never", *color)
|
||||
@@ -80,6 +88,9 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
||||
signal.Notify(make(chan os.Signal, 1), syscall.SIGPIPE)
|
||||
var gone error
|
||||
emit := func(e listing.Entry) error {
|
||||
if e.Error != "" && !*showErrors {
|
||||
return nil
|
||||
}
|
||||
if err := write(e); err != nil {
|
||||
gone = err
|
||||
return err
|
||||
@@ -91,8 +102,8 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
||||
if cachePath != "" {
|
||||
f = cachefile.Read(cachePath)
|
||||
}
|
||||
f, errs := listing.Run(context.Background(), listing.Options{
|
||||
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Timeout: timeout,
|
||||
f = listing.Run(context.Background(), listing.Options{
|
||||
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Kinds: only, Timeout: timeout,
|
||||
}, emit)
|
||||
if cachePath != "" && !*cached {
|
||||
byID := map[string]provider.Provider{}
|
||||
@@ -101,23 +112,18 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
||||
}
|
||||
keep := func(id string) bool { p, ok := byID[id]; return ok && p.Kind() != provider.KindRepo }
|
||||
if err := cachefile.Save(cachePath, f, keep, time.Now()); err != nil {
|
||||
errs = append(errs, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if gone != nil {
|
||||
if errors.Is(gone, syscall.EPIPE) {
|
||||
return nil // the reader has what it wanted
|
||||
}
|
||||
if gone != nil && !errors.Is(gone, syscall.EPIPE) {
|
||||
return gone
|
||||
}
|
||||
for _, err := range errs {
|
||||
fmt.Fprintln(errOut, "lazysecrets: list:", err)
|
||||
}
|
||||
return nil
|
||||
return nil // or the reader has what it wanted
|
||||
}
|
||||
|
||||
// nameOf is an entry's name for the text format: the provider's icon,
|
||||
// the path with the item's own name standing out, and the account.
|
||||
// the path with the item's own name standing out, and the account. A
|
||||
// list that couldn't be read has ✗ for the icon, and why after it.
|
||||
func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool) string {
|
||||
render := func(st lipgloss.Style, s string) string {
|
||||
if !color || s == "" {
|
||||
@@ -132,6 +138,10 @@ func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool)
|
||||
}
|
||||
}
|
||||
icon := iconSet.Prefix(kind)
|
||||
iconStyle := theme.Kind(kind)
|
||||
if e.Error != "" {
|
||||
icon, iconStyle = "✗ ", theme.Error
|
||||
}
|
||||
var name string
|
||||
if n := len(e.Path); n > 0 {
|
||||
if n > 1 {
|
||||
@@ -142,5 +152,8 @@ func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool)
|
||||
if e.Account != "" {
|
||||
name += render(theme.Muted, " · "+e.Account)
|
||||
}
|
||||
return render(theme.Kind(kind), strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
|
||||
if e.Error != "" {
|
||||
name += " " + render(theme.Error, e.Error)
|
||||
}
|
||||
return render(iconStyle, strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
|
||||
}
|
||||
@@ -23,3 +23,14 @@ func TestListNames(t *testing.T) {
|
||||
t.Errorf("name not set apart: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListErrorNames(t *testing.T) {
|
||||
e := listing.Entry{Provider: "azure", Path: []string{"platform-prod", "kv-restricted"}, Error: "403 Forbidden"}
|
||||
if got := nameOf(e, styles.Default(), icons.NerdFont, false); got != "✗ platform-prod / kv-restricted 403 Forbidden" {
|
||||
t.Errorf("plain: %q", got)
|
||||
}
|
||||
red := styles.Default().Error.Render("403 Forbidden")
|
||||
if got := nameOf(e, styles.Default(), icons.NerdFont, true); !strings.HasSuffix(got, red) {
|
||||
t.Errorf("why isn't in the error colour: %q", got)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"golang.org/x/term"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/commands"
|
||||
@@ -122,8 +121,8 @@ func run() error {
|
||||
{{secrets/scope/key}}
|
||||
lazysecrets [flags] get <ref> print the value a lazysecrets:// link points at,
|
||||
for scripts: VAR=$(lazysecrets get <ref>)
|
||||
lazysecrets [flags] list [--cached] [--max-age 2m] [--format text|json]
|
||||
[--color always|never] [--icons=false] [--errors]
|
||||
lazysecrets [flags] list [--kind secret,…] [--cached] [--max-age 2m] [--format text|json]
|
||||
[--color always|never] [--icons=false] [--errors=false]
|
||||
print every source, vault and secret it can
|
||||
find, one per line, for fzf and pickers:
|
||||
what's cached at once, then what loads
|
||||
@@ -358,12 +357,7 @@ Flags:
|
||||
if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) {
|
||||
cachePath = filepath.Join(d, "cache.json")
|
||||
}
|
||||
// Into a pipe (fzf), stderr would draw over what reads it.
|
||||
errOut := io.Discard
|
||||
if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||
errOut = os.Stderr
|
||||
}
|
||||
return listCmd(providers, cachePath, timeoutFor, theme, *icons, listArgs, os.Stdout, errOut, os.Stderr)
|
||||
return listCmd(providers, cachePath, timeoutFor, theme, *icons, listArgs, os.Stdout)
|
||||
}
|
||||
if shareArgs != nil {
|
||||
// The files from a repo, the key from whichever provider it's
|
||||
|
||||
Reference in new issue
Block a user