From 824f7249c3a9e9ba1797aefa3ba2862d2014d742 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karl-Einar=20Bergstr=C3=B8m?= Date: Wed, 14 Oct 2026 16:36:00 +0200 Subject: [PATCH] feat: lazysecrets list prints notes, and --demo has a couple --- cmd/lazysecrets/list.go | 13 ++++++++----- internal/listing/listing.go | 10 ++++++---- internal/listing/listing_test.go | 9 ++++++--- internal/provider/fake/seed.go | 17 +++++++++++------ 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/cmd/lazysecrets/list.go b/cmd/lazysecrets/list.go index 2342e31..3a3531a 100644 --- a/cmd/lazysecrets/list.go +++ b/cmd/lazysecrets/list.go @@ -31,7 +31,8 @@ import ( // // 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 -// 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 // name is coloured from the theme, // 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) 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: 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") 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)") @@ -91,8 +92,10 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t if status == "" && *expiry { 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. - _, 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 } case "json": @@ -124,9 +127,9 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t } f = listing.Run(context.Background(), listing.Options{ 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) - return n.StarredFor(account), n.LastUsed, n.Uses + return n.StarredFor(account), n.LastUsed, n.Uses, n.Note }, }, emit) if cachePath != "" && !*cached { diff --git a/internal/listing/listing.go b/internal/listing/listing.go index 8f32e9b..ef9b4c2 100644 --- a/internal/listing/listing.go +++ b/internal/listing/listing.go @@ -49,6 +49,8 @@ type Entry struct { // Enabled whether it's enabled, where the provider says (Azure). Expires time.Time `json:"expires,omitzero"` 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 // listed. The entry is then that source or vault, again, or with an // 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. Kinds []string // Mark tells whether the entry with link is starred for account, - // and when it was last used and how often (zero: not lately); nil - // marks nothing. - Mark func(link, account string) (starred bool, lastUsed time.Time, uses int) + // when it was last used and how often (zero: not lately), and its + // note; nil marks nothing. + Mark func(link, account string) (starred bool, lastUsed time.Time, uses int, note string) Timeout func(provider.Kind) time.Duration Now func() time.Time } @@ -233,7 +235,7 @@ func (l *lister) put(p provider.Provider, e Entry) { } e.Account = p.ID() 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() } id := p.ID() + "\x00" + e.Link + "\x00" + e.Error diff --git a/internal/listing/listing_test.go b/internal/listing/listing_test.go index f639480..f09b019 100644 --- a/internal/listing/listing_test.go +++ b/internal/listing/listing_test.go @@ -229,14 +229,14 @@ func TestListMarks(t *testing.T) { 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, time.Time, int) { + Mark: func(link, account string) (bool, time.Time, int, string) { if starredLink == "" && strings.HasSuffix(link, "/db-password") { starredLink = link } 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 for _, e := range got { @@ -246,6 +246,9 @@ func TestListMarks(t *testing.T) { 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 { recent++ if e.Uses != 3 || e.LastUsed.IsZero() { diff --git a/internal/provider/fake/seed.go b/internal/provider/fake/seed.go index 5176ee9..cd9e237 100644 --- a/internal/provider/fake/seed.go +++ b/internal/provider/fake/seed.go @@ -8,13 +8,14 @@ import ( "git.kebcloud.no/keb/lazysecrets/internal/store" ) -// Seed stars and uses a few of the demo's secrets, so --demo shows -// Favourites (F), Recent (R) and stars in lazysecrets list. alice and +// Seed stars, notes and uses a few of the demo's secrets, so --demo +// shows Favourites (F), Recent (R), notes, and stars in lazysecrets list. alice and // admin are the two Azure accounts' provider IDs. func Seed(st *store.Store, alice, admin string) { type item struct { link, account, source string name string // else the link's last part + note string path []string star bool 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/" for _, it := range []item{ {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", 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", - 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", path: []string{"platform-prod", "kv-shared-prod"}, uses: 2, ago: 26 * time.Hour}, {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.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) { context(n) - n.ToggleStar(it.account) + if it.star { + n.ToggleStar(it.account) + } + n.Note = it.note }) } for range it.uses {