feat: lazysecrets list prints everything lazysecrets can find, for fzf and editor pickers #70
No files matched your search
+22
-1
@@ -19,6 +19,7 @@ import (
|
|||||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
||||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
||||||
)
|
)
|
||||||
@@ -39,13 +40,14 @@ import (
|
|||||||
// follows as it loads. A list that can't be read is a line too, as it
|
// follows as it loads. A list that can't be read is a line too, as it
|
||||||
// fails: the vault or source, then ✗ and why in the theme's error colour.
|
// fails: the vault or source, then ✗ and why in the theme's error colour.
|
||||||
// They're in the output rather than on stderr, which would draw over fzf.
|
// 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 {
|
func listCmd(providers []provider.Provider, st *store.Store, 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)
|
fs := flag.NewFlagSet("list", flag.ContinueOnError)
|
||||||
cached := fs.Bool("cached", false, "only what's cached, and repos: load nothing from Azure or Databricks")
|
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")
|
maxAge := fs.Duration("max-age", 2*time.Minute, "load cached lists older than this again")
|
||||||
format := fs.String("format", "text", "text: kind, name, error, account and link, tab-separated; json: an object per line")
|
format := fs.String("format", "text", "text: kind, name, error, account and link, tab-separated; json: an object per line")
|
||||||
color := fs.String("color", "always", "colour the names (text): always or never; fzf needs --ansi")
|
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)")
|
iconsOn := fs.Bool("icons", withIcons, "put the provider's Nerd Font icon before names (text)")
|
||||||
|
starIcon := fs.Bool("starred-icon", false, "put ★ before starred names, in the theme's favouriteColor (text)")
|
||||||
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
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\")")
|
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 {
|
if err := fs.Parse(args); err != nil {
|
||||||
@@ -77,6 +79,9 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
|||||||
write = func(e listing.Entry) error {
|
write = func(e listing.Entry) error {
|
||||||
// A tab or line break in a name would break the line up.
|
// A tab or line break in a name would break the line up.
|
||||||
name := nameOf(e, theme, iconSet, *color == "always")
|
name := nameOf(e, theme, iconSet, *color == "always")
|
||||||
|
if *starIcon {
|
||||||
|
name = starOf(e, theme, *color == "always") + name
|
||||||
|
}
|
||||||
why := errorOf(e, theme, *color == "always")
|
why := errorOf(e, theme, *color == "always")
|
||||||
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\n", e.Kind, oneLine.Replace(name), oneLine.Replace(why), e.Account, e.Link)
|
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\n", e.Kind, oneLine.Replace(name), oneLine.Replace(why), e.Account, e.Link)
|
||||||
return err
|
return err
|
||||||
@@ -110,6 +115,10 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
|||||||
}
|
}
|
||||||
f = listing.Run(context.Background(), listing.Options{
|
f = listing.Run(context.Background(), listing.Options{
|
||||||
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Kinds: only, Timeout: timeout,
|
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Kinds: only, Timeout: timeout,
|
||||||
|
Mark: func(link, account string) (bool, bool) {
|
||||||
|
n := st.Node(link)
|
||||||
|
return n.StarredFor(account), !n.LastUsed.IsZero()
|
||||||
|
},
|
||||||
}, emit)
|
}, emit)
|
||||||
if cachePath != "" && !*cached {
|
if cachePath != "" && !*cached {
|
||||||
byID := map[string]provider.Provider{}
|
byID := map[string]provider.Provider{}
|
||||||
@@ -129,6 +138,18 @@ func listCmd(providers []provider.Provider, cachePath string, timeout func(provi
|
|||||||
|
|
||||||
var oneLine = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ", "\r", " ")
|
var oneLine = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ", "\r", " ")
|
||||||
|
|
||||||
|
// starOf is ★ for a starred entry, as the TUI marks them, and spaces
|
||||||
|
// the same width for the rest, so names line up.
|
||||||
|
func starOf(e listing.Entry, theme styles.Styles, color bool) string {
|
||||||
|
switch {
|
||||||
|
case !e.Starred:
|
||||||
|
return " "
|
||||||
|
case color:
|
||||||
|
return theme.Favourite.Render("★") + " "
|
||||||
|
}
|
||||||
|
return "★ "
|
||||||
|
}
|
||||||
|
|
||||||
// errorOf is why a list couldn't be read, or "": with colour, a red ✗
|
// errorOf is why a list couldn't be read, or "": with colour, a red ✗
|
||||||
// before it, as the TUI marks lists it can't read.
|
// before it, as the TUI marks lists it can't read.
|
||||||
func errorOf(e listing.Entry, theme styles.Styles, color bool) string {
|
func errorOf(e listing.Entry, theme styles.Styles, color bool) string {
|
||||||
|
|||||||
@@ -39,3 +39,15 @@ func TestListErrorNames(t *testing.T) {
|
|||||||
t.Errorf("no error: %q", got)
|
t.Errorf("no error: %q", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListStars(t *testing.T) {
|
||||||
|
if got := starOf(listing.Entry{Starred: true}, styles.Default(), false); got != "★ " {
|
||||||
|
t.Errorf("starred: %q", got)
|
||||||
|
}
|
||||||
|
if got := starOf(listing.Entry{}, styles.Default(), true); got != " " {
|
||||||
|
t.Errorf("not starred: %q, want spaces as wide as ★", got)
|
||||||
|
}
|
||||||
|
if got := starOf(listing.Entry{Starred: true}, styles.Default(), true); got != styles.Default().Favourite.Render("★")+" " {
|
||||||
|
t.Errorf("not in the favourite colour: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -385,7 +385,7 @@ Flags:
|
|||||||
if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) {
|
if d, err := config.CacheDir(); err == nil && !*demo && config.On(cfg.Storage.Cache) {
|
||||||
cachePath = filepath.Join(d, "cache.json")
|
cachePath = filepath.Join(d, "cache.json")
|
||||||
}
|
}
|
||||||
return listCmd(providers, cachePath, timeoutFor, theme, *icons, listArgs, os.Stdout)
|
return listCmd(providers, st, cachePath, timeoutFor, theme, *icons, listArgs, os.Stdout)
|
||||||
}
|
}
|
||||||
if shareArgs != nil {
|
if shareArgs != nil {
|
||||||
// The files from a repo, the key from whichever provider it's
|
// The files from a repo, the key from whichever provider it's
|
||||||
|
|||||||
@@ -39,6 +39,10 @@ type Entry struct {
|
|||||||
Account string `json:"account"`
|
Account string `json:"account"`
|
||||||
// AccountLabel names the az account, when there are several.
|
// AccountLabel names the az account, when there are several.
|
||||||
AccountLabel string `json:"accountLabel,omitempty"`
|
AccountLabel string `json:"accountLabel,omitempty"`
|
||||||
|
// Starred is starred for this account; Recent is used lately (Recent,
|
||||||
|
// R, in the TUI).
|
||||||
|
Starred bool `json:"starred"`
|
||||||
|
Recent bool `json:"recent"`
|
||||||
// Error says why a source's vaults or a vault's secrets couldn't be
|
// Error says why a source's vaults or a vault's secrets couldn't be
|
||||||
// listed. The entry is then that source or vault, again, or with an
|
// listed. The entry is then that source or vault, again, or with an
|
||||||
// account's sources unreadable, the account without a link.
|
// account's sources unreadable, the account without a link.
|
||||||
@@ -56,7 +60,10 @@ type Options struct {
|
|||||||
// Kinds are the kinds of entry to list (Kinds' names); none is all.
|
// Kinds are the kinds of entry to list (Kinds' names); none is all.
|
||||||
// Lists only needed for other kinds aren't loaded: sources and vaults
|
// Lists only needed for other kinds aren't loaded: sources and vaults
|
||||||
// alone don't load any secrets. Lists that fail are always listed.
|
// alone don't load any secrets. Lists that fail are always listed.
|
||||||
Kinds []string
|
Kinds []string
|
||||||
|
// Mark tells whether the entry with link is starred for account, and
|
||||||
|
// used lately; nil marks nothing.
|
||||||
|
Mark func(link, account string) (starred, recent bool)
|
||||||
Timeout func(provider.Kind) time.Duration
|
Timeout func(provider.Kind) time.Duration
|
||||||
Now func() time.Time
|
Now func() time.Time
|
||||||
}
|
}
|
||||||
@@ -217,6 +224,9 @@ func (l *lister) put(p provider.Provider, e Entry) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
e.Account = p.ID()
|
e.Account = p.ID()
|
||||||
|
if l.o.Mark != nil && e.Link != "" {
|
||||||
|
e.Starred, e.Recent = l.o.Mark(e.Link, e.Account)
|
||||||
|
}
|
||||||
id := p.ID() + "\x00" + e.Link + "\x00" + e.Error
|
id := p.ID() + "\x00" + e.Link + "\x00" + e.Error
|
||||||
if l.stopped != nil || l.seen[id] {
|
if l.stopped != nil || l.seen[id] {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -223,3 +223,31 @@ func TestErrorsAreOneLine(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestListMarks(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
az := fake.Azure(0)
|
||||||
|
var starredLink string
|
||||||
|
got, _, _ := run(t, Options{Providers: []provider.Provider{az}, MaxAge: time.Minute, Kinds: []string{"secret"},
|
||||||
|
Mark: func(link, account string) (bool, bool) {
|
||||||
|
if starredLink == "" && strings.HasSuffix(link, "/db-password") {
|
||||||
|
starredLink = link
|
||||||
|
}
|
||||||
|
return link == starredLink && account == az.ID(), strings.HasSuffix(link, "/api-key")
|
||||||
|
}})
|
||||||
|
var starred, recent int
|
||||||
|
for _, e := range got {
|
||||||
|
if e.Starred {
|
||||||
|
starred++
|
||||||
|
if e.Link != starredLink {
|
||||||
|
t.Errorf("%s starred", e.Link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if e.Recent {
|
||||||
|
recent++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if starred != 1 || recent == 0 {
|
||||||
|
t.Errorf("%d starred, %d recent", starred, recent)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user