feat: lazysecrets list prints notes, and --demo has a couple

This commit is contained in:
keb committed 2026-10-14 21:24:01 +02:00
1 parent 2c9c37f809
commit 824f7249c3
4 files changed
+30 -17

No files matched your search

+8 -5
View File
@@ -31,7 +31,8 @@ import (
// //
// Each line is the kind, a mark (✗ or ⚠, ★, ◷ and the provider's icon), // Each line is the kind, a mark (✗ or ⚠, ★, ◷ and the provider's icon),
// the name, a status (why it couldn't be read, or that it has expired or // the name, a status (why it couldn't be read, or that it has expired or
// expires soon; else empty), the account and the link, tab-separated. The mark has a field of its own so a search can // expires soon; else empty), its note (muted, or empty), the account and
// the link, tab-separated. The mark has a field of its own so a search can
// leave it out (fzf --nth), and ^prod finds names starting with prod. The // leave it out (fzf --nth), and ^prod finds names starting with prod. The
// name is coloured from the theme, // name is coloured from the theme,
// with the provider's icon (as in the TUI: --icons, LAZYSECRETS_ICONS, // with the provider's icon (as in the TUI: --icons, LAZYSECRETS_ICONS,
@@ -45,7 +46,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
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, mark, name, status, account and link, tab-separated; json: an object per line") format := fs.String("format", "text", "text: kind, mark, name, status, note, 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)")
stars := fs.Bool("stars", true, "put ★ before starred names, in the theme's favouriteColor (text)") stars := fs.Bool("stars", true, "put ★ before starred names, in the theme's favouriteColor (text)")
@@ -91,8 +92,10 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
if status == "" && *expiry { if status == "" && *expiry {
status = expiryOf(e, theme, expiryWarn, now, colored) status = expiryOf(e, theme, expiryWarn, now, colored)
} }
note := renderer(colored)(theme.Muted, e.Note)
// 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.
_, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\n", e.Kind, mark, oneLine.Replace(name), oneLine.Replace(status), e.Account, e.Link) _, err := fmt.Fprintf(out, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", e.Kind, mark, oneLine.Replace(name), oneLine.Replace(status),
oneLine.Replace(note), e.Account, e.Link)
return err return err
} }
case "json": case "json":
@@ -124,9 +127,9 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
} }
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, time.Time, int) { Mark: func(link, account string) (bool, time.Time, int, string) {
n := st.Node(link) n := st.Node(link)
return n.StarredFor(account), n.LastUsed, n.Uses return n.StarredFor(account), n.LastUsed, n.Uses, n.Note
}, },
}, emit) }, emit)
if cachePath != "" && !*cached { if cachePath != "" && !*cached {
+6 -4
View File
@@ -49,6 +49,8 @@ type Entry struct {
// Enabled whether it's enabled, where the provider says (Azure). // Enabled whether it's enabled, where the provider says (Azure).
Expires time.Time `json:"expires,omitzero"` Expires time.Time `json:"expires,omitzero"`
Enabled *bool `json:"enabled,omitempty"` Enabled *bool `json:"enabled,omitempty"`
// Note is the user's note on it, if any.
Note string `json:"note,omitempty"`
// 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.
@@ -68,9 +70,9 @@ type Options struct {
// 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, // Mark tells whether the entry with link is starred for account,
// and when it was last used and how often (zero: not lately); nil // when it was last used and how often (zero: not lately), and its
// marks nothing. // note; nil marks nothing.
Mark func(link, account string) (starred bool, lastUsed time.Time, uses int) Mark func(link, account string) (starred bool, lastUsed time.Time, uses int, note string)
Timeout func(provider.Kind) time.Duration Timeout func(provider.Kind) time.Duration
Now func() time.Time Now func() time.Time
} }
@@ -233,7 +235,7 @@ func (l *lister) put(p provider.Provider, e Entry) {
} }
e.Account = p.ID() e.Account = p.ID()
if l.o.Mark != nil && e.Link != "" { if l.o.Mark != nil && e.Link != "" {
e.Starred, e.LastUsed, e.Uses = l.o.Mark(e.Link, e.Account) e.Starred, e.LastUsed, e.Uses, e.Note = l.o.Mark(e.Link, e.Account)
e.Recent = !e.LastUsed.IsZero() e.Recent = !e.LastUsed.IsZero()
} }
id := p.ID() + "\x00" + e.Link + "\x00" + e.Error id := p.ID() + "\x00" + e.Link + "\x00" + e.Error
+6 -3
View File
@@ -229,14 +229,14 @@ func TestListMarks(t *testing.T) {
az := fake.Azure(0) az := fake.Azure(0)
var starredLink string var starredLink string
got, _, _ := run(t, Options{Providers: []provider.Provider{az}, MaxAge: time.Minute, Kinds: []string{"secret"}, got, _, _ := run(t, Options{Providers: []provider.Provider{az}, MaxAge: time.Minute, Kinds: []string{"secret"},
Mark: func(link, account string) (bool, time.Time, int) { Mark: func(link, account string) (bool, time.Time, int, string) {
if starredLink == "" && strings.HasSuffix(link, "/db-password") { if starredLink == "" && strings.HasSuffix(link, "/db-password") {
starredLink = link starredLink = link
} }
if strings.HasSuffix(link, "/api-key") { if strings.HasSuffix(link, "/api-key") {
return false, time.Now(), 3 return false, time.Now(), 3, "rotate in May"
} }
return link == starredLink && account == az.ID(), time.Time{}, 0 return link == starredLink && account == az.ID(), time.Time{}, 0, ""
}}) }})
var starred, recent int var starred, recent int
for _, e := range got { for _, e := range got {
@@ -246,6 +246,9 @@ func TestListMarks(t *testing.T) {
t.Errorf("%s starred", e.Link) t.Errorf("%s starred", e.Link)
} }
} }
if (e.Note != "") != strings.HasSuffix(e.Link, "/api-key") {
t.Errorf("%s: note %q", e.Link, e.Note)
}
if e.Recent { if e.Recent {
recent++ recent++
if e.Uses != 3 || e.LastUsed.IsZero() { if e.Uses != 3 || e.LastUsed.IsZero() {
+10 -5
View File
@@ -8,13 +8,14 @@ import (
"git.kebcloud.no/keb/lazysecrets/internal/store" "git.kebcloud.no/keb/lazysecrets/internal/store"
) )
// Seed stars and uses a few of the demo's secrets, so --demo shows // Seed stars, notes and uses a few of the demo's secrets, so --demo
// Favourites (F), Recent (R) and stars in lazysecrets list. alice and // shows Favourites (F), Recent (R), notes, and stars in lazysecrets list. alice and
// admin are the two Azure accounts' provider IDs. // admin are the two Azure accounts' provider IDs.
func Seed(st *store.Store, alice, admin string) { func Seed(st *store.Store, alice, admin string) {
type item struct { type item struct {
link, account, source string link, account, source string
name string // else the link's last part name string // else the link's last part
note string
path []string path []string
star bool star bool
uses int uses int
@@ -23,11 +24,12 @@ func Seed(st *store.Store, alice, admin string) {
const azure, dbx = "lazysecrets://azure/", "lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/" const azure, dbx = "lazysecrets://azure/", "lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/"
for _, it := range []item{ for _, it := range []item{
{link: azure + "kv-app-prod.vault.azure.net/secrets/db-password", account: alice, source: "sub-prod", {link: azure + "kv-app-prod.vault.azure.net/secrets/db-password", account: alice, source: "sub-prod",
path: []string{"platform-prod", "kv-app-prod"}, star: true, uses: 12, ago: 20 * time.Minute}, path: []string{"platform-prod", "kv-app-prod"}, star: true, uses: 12, ago: 20 * time.Minute,
note: "rotated by the data team each quarter"},
{link: azure + "kv-app-prod.vault.azure.net/secrets/api-key", account: alice, source: "sub-prod", {link: azure + "kv-app-prod.vault.azure.net/secrets/api-key", account: alice, source: "sub-prod",
path: []string{"platform-prod", "kv-app-prod"}, uses: 4, ago: 2 * time.Hour}, path: []string{"platform-prod", "kv-app-prod"}, uses: 4, ago: 2 * time.Hour},
{link: azure + "kv-breakglass.vault.azure.net/secrets/emergency-admin", account: admin, source: "sub-prod", {link: azure + "kv-breakglass.vault.azure.net/secrets/emergency-admin", account: admin, source: "sub-prod",
path: []string{"platform-prod", "kv-breakglass"}, star: true}, path: []string{"platform-prod", "kv-breakglass"}, star: true, note: "incidents only: tell #ops when used"},
{link: azure + "kv-shared-prod.vault.azure.net/secrets/smtp-password", account: alice, source: "sub-prod", {link: azure + "kv-shared-prod.vault.azure.net/secrets/smtp-password", account: alice, source: "sub-prod",
path: []string{"platform-prod", "kv-shared-prod"}, uses: 2, ago: 26 * time.Hour}, path: []string{"platform-prod", "kv-shared-prod"}, uses: 2, ago: 26 * time.Hour},
{link: dbx + "app/storage-key", account: "Databricks", source: "dbx-prod", {link: dbx + "app/storage-key", account: "Databricks", source: "dbx-prod",
@@ -43,10 +45,13 @@ func Seed(st *store.Store, alice, admin string) {
n.Name = cmp.Or(it.name, it.link[strings.LastIndex(it.link, "/")+1:]) n.Name = cmp.Or(it.name, it.link[strings.LastIndex(it.link, "/")+1:])
n.Path, n.Source, n.Account = it.path, it.source, it.account n.Path, n.Source, n.Account = it.path, it.source, it.account
} }
if it.star { if it.star || it.note != "" {
_ = st.Update(it.link, func(n *store.Node) { _ = st.Update(it.link, func(n *store.Node) {
context(n) context(n)
if it.star {
n.ToggleStar(it.account) n.ToggleStar(it.account)
}
n.Note = it.note
}) })
} }
for range it.uses { for range it.uses {