fix: review findings for lazysecrets list: recent means the last 90 days, cache saves lock and keep deletions and the TUI's cursors, failed lists aren't retried, one expiry rule
This commit is contained in:
23 files changed
+333
-107
No files matched your search
@@ -213,9 +213,12 @@ type Model struct {
|
||||
statusW int // the status pane's width: the left column's, zoomed or not
|
||||
changelog []panes.Field
|
||||
cacheDirty bool
|
||||
sourcesAt map[string]time.Time
|
||||
placeDir string
|
||||
savedPlace string // the link last saved as the place
|
||||
// cacheDropped are cache keys deleted since the last save, so saving
|
||||
// deletes them from the file too.
|
||||
cacheDropped map[string]bool
|
||||
sourcesAt map[string]time.Time
|
||||
placeDir string
|
||||
savedPlace string // the link last saved as the place
|
||||
|
||||
// A pending clear of a copied value, counted down in the footer.
|
||||
// clipToken identifies the latest copy so older ticks are ignored.
|
||||
@@ -688,7 +691,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if src, ok := m.selectedSource(); ok {
|
||||
m.logErrOf(src.Name, msg.err)
|
||||
}
|
||||
delete(m.cache.containers, msg.key)
|
||||
uncache(&m, m.cache.containers, msg.key)
|
||||
m.containers.SetError(msg.err)
|
||||
} else {
|
||||
m.cache.containers[msg.key] = entry[[]provider.Container]{msg.items, m.now()}
|
||||
@@ -709,7 +712,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
if c, ok := m.containers.Selected(); ok {
|
||||
m.logErrOf(c.Name, msg.err)
|
||||
}
|
||||
delete(m.cache.objects, msg.key)
|
||||
uncache(&m, m.cache.objects, msg.key)
|
||||
m.secrets.SetError(msg.err)
|
||||
} else {
|
||||
if msg.bound != nil {
|
||||
|
||||
@@ -67,10 +67,23 @@ func (m *Model) refreshShown(before []provider.Object) tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
m.detail.Refresh(s)
|
||||
delete(m.cache.details, detailKey(s))
|
||||
uncache(m, m.cache.details, detailKey(s))
|
||||
return m.describe(s)
|
||||
}
|
||||
|
||||
// uncache deletes key from a cache, and, at the next save, from the file.
|
||||
func uncache[T any](m *Model, c map[string]entry[T], key string) {
|
||||
if _, ok := c[key]; !ok {
|
||||
return
|
||||
}
|
||||
delete(c, key)
|
||||
if m.cacheDropped == nil {
|
||||
m.cacheDropped = map[string]bool{}
|
||||
}
|
||||
m.cacheDropped[key] = true
|
||||
m.cacheDirty = true
|
||||
}
|
||||
|
||||
// sameListing reports whether a and b list the same: what a list
|
||||
// says of an object, not what describing it adds.
|
||||
func sameListing(a, b provider.Object) bool {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
|
||||
@@ -28,7 +29,7 @@ func (m Model) expiringItems() []expiringItem {
|
||||
row := sourceRowKey(p.ID(), src.ID)
|
||||
for _, c := range m.cache.containers[row].value {
|
||||
for _, t := range c.ObjectTypes() {
|
||||
for _, o := range m.cache.objects[containerKey(c)+"|"+string(t)].value {
|
||||
for _, o := range m.cache.objects[cachefile.ObjectsKey(c, t)].value {
|
||||
if state, _ := m.expiry(o); state != notExpiring {
|
||||
out = append(out, expiringItem{o, row})
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/expiry"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
)
|
||||
|
||||
@@ -11,28 +12,18 @@ import (
|
||||
// list once they have expired (✗ expired) or will within the configured
|
||||
// warning period (⚠ 12d), and typing "expir" in the filter finds them.
|
||||
|
||||
type expiryState int
|
||||
type expiryState = expiry.State
|
||||
|
||||
const (
|
||||
notExpiring expiryState = iota
|
||||
expiresSoon
|
||||
expired
|
||||
notExpiring = expiry.None
|
||||
expiresSoon = expiry.Soon
|
||||
expired = expiry.Expired
|
||||
)
|
||||
|
||||
// expiry tells whether o has expired or will within the warning period,
|
||||
// and how long it has left.
|
||||
func (m Model) expiry(o provider.Object) (expiryState, time.Duration) {
|
||||
if o.Expires.IsZero() {
|
||||
return notExpiring, 0
|
||||
}
|
||||
left := o.Expires.Sub(m.now())
|
||||
switch {
|
||||
case left <= 0:
|
||||
return expired, 0
|
||||
case left <= m.expiryWarning:
|
||||
return expiresSoon, left
|
||||
}
|
||||
return notExpiring, left
|
||||
return expiry.Of(o.Expires, m.now(), m.expiryWarning)
|
||||
}
|
||||
|
||||
// expiryWord is matched by the filter: "expired" or "expiring".
|
||||
@@ -47,9 +38,4 @@ func (m Model) expiryWord(o provider.Object) string {
|
||||
}
|
||||
|
||||
// daysLeft is a short "12d", or "5h" on the last day.
|
||||
func daysLeft(d time.Duration) string {
|
||||
if d < 24*time.Hour {
|
||||
return fmt.Sprintf("%dh", max(1, int(d.Hours())))
|
||||
}
|
||||
return fmt.Sprintf("%dd", int(d.Hours()/24))
|
||||
}
|
||||
func daysLeft(d time.Duration) string { return expiry.Left(d) }
|
||||
@@ -3,9 +3,11 @@ package app
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||
@@ -24,12 +26,12 @@ const recentMark = "◷ "
|
||||
|
||||
// marks is what goes before a list row's name: ★ when it's starred for
|
||||
// account and, with recent, ◷ when it was used lately.
|
||||
func marks(st styles.Styles, n store.Node, account string, recent bool) string {
|
||||
func marks(st styles.Styles, n store.Node, account string, recent bool, now time.Time) string {
|
||||
var s string
|
||||
if n.StarredFor(account) {
|
||||
s = st.Favourite.Render(star)
|
||||
}
|
||||
if recent && !n.LastUsed.IsZero() {
|
||||
if recent && n.UsedLately(now) {
|
||||
s += st.Muted.Render(recentMark)
|
||||
}
|
||||
return s
|
||||
@@ -110,7 +112,7 @@ func (m Model) containerRender(withSource bool) func(styles.Styles, provider.Con
|
||||
}
|
||||
line += sty.Muted.Render(" " + c.Detail)
|
||||
}
|
||||
line = marks(sty, n, c.Source.Provider, m.recentIcon) + line
|
||||
line = marks(sty, n, c.Source.Provider, m.recentIcon, m.now()) + line
|
||||
if withSource {
|
||||
src := c.Source.Name
|
||||
if account, ok := strings.CutPrefix(c.Source.ProviderLabel, string(c.Source.Kind)+" · "); ok {
|
||||
@@ -130,7 +132,7 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string {
|
||||
if o.Enabled != nil && !*o.Enabled {
|
||||
line = sty.Muted.Render(o.Name + " (disabled)")
|
||||
}
|
||||
line = marks(sty, n, o.Container.Source.Provider, m.recentIcon) + line
|
||||
line = marks(sty, n, o.Container.Source.Provider, m.recentIcon, m.now()) + line
|
||||
switch state, left := m.expiry(o); state {
|
||||
case expired:
|
||||
line += " " + sty.Error.Render("✗ expired")
|
||||
@@ -179,7 +181,7 @@ func (m *Model) rebind(c provider.Container) string {
|
||||
m.linked[i].Source = c.Source
|
||||
}
|
||||
}
|
||||
key := containerKey(c) + "|" + string(m.objType)
|
||||
key := cachefile.ObjectsKey(c, m.objType)
|
||||
if m.onLinks() {
|
||||
m.containers.SetItems(m.linked)
|
||||
m.containers.Select(containerKey(c))
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
|
||||
)
|
||||
@@ -160,7 +161,7 @@ func (m Model) containerFields(c provider.Container) []panes.Field {
|
||||
add("source", cmpOr(c.Source.ProviderLabel, string(c.Source.Kind))+" / "+c.Source.Name)
|
||||
add("web", c.URL)
|
||||
for _, t := range c.ObjectTypes() {
|
||||
if e, ok := m.cache.objects[containerKey(c)+"|"+string(t)]; ok {
|
||||
if e, ok := m.cache.objects[cachefile.ObjectsKey(c, t)]; ok {
|
||||
add(strings.ToLower(string(t))+"s", fmt.Sprint(len(e.value)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,7 +107,7 @@ func (m *Model) storeEntries() (favs, recent []option) {
|
||||
if m.history {
|
||||
var used []string
|
||||
for _, k := range keys {
|
||||
if !nodes[k].LastUsed.IsZero() {
|
||||
if nodes[k].UsedLately(m.now()) {
|
||||
used = append(used, k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,14 +33,7 @@ type persistedMsg struct{ err error }
|
||||
// isn't a repo: every key starts with the provider ID.
|
||||
func (m *Model) keepCached(key string) bool { return m.keepProvider(cachefile.ProviderOf(key)) }
|
||||
|
||||
func (m *Model) keepProvider(id string) bool { return keepIn(m.byID)(id) }
|
||||
|
||||
func keepIn(byID map[string]provider.Provider) func(id string) bool {
|
||||
return func(id string) bool {
|
||||
p, ok := byID[id]
|
||||
return ok && p.Kind() != provider.KindRepo
|
||||
}
|
||||
}
|
||||
func (m *Model) keepProvider(id string) bool { return cachefile.Keep(m.providers)(id) }
|
||||
|
||||
// loadCacheFile fills the cache from the file, leaving out what's too
|
||||
// old or belongs to accounts that aren't signed in any more.
|
||||
@@ -79,6 +72,7 @@ func (m *Model) loadCacheFile() {
|
||||
// cacheSnapshot is the cache as the file holds it.
|
||||
func (m *Model) cacheSnapshot() cachefile.File {
|
||||
f := cachefile.New()
|
||||
f.Dropped, m.cacheDropped = m.cacheDropped, nil
|
||||
for id, r := range m.sourceResults {
|
||||
if r.err == nil && m.keepProvider(id) {
|
||||
f.Sources[id] = cachefile.Stamped[[]provider.Source]{At: m.sourcesAt[id], Value: r.sources}
|
||||
@@ -110,7 +104,7 @@ func (m *Model) cacheSnapshot() cachefile.File {
|
||||
// saveCache saves f, merged with the file (lazysecrets list may have
|
||||
// loaded more since), from any goroutine.
|
||||
func (m *Model) saveCache(f cachefile.File) func() error {
|
||||
path, keep, now := m.cachePath, keepIn(m.byID), m.now()
|
||||
path, keep, now := m.cachePath, cachefile.Keep(m.providers), m.now()
|
||||
return func() error { return cachefile.Save(path, f, keep, now) }
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
)
|
||||
@@ -89,7 +90,7 @@ func (m *Model) searchEntries() (sources, items []option) {
|
||||
return nil
|
||||
}, reload)
|
||||
for _, t := range []provider.ObjectType{provider.TypeSecret, provider.TypeKey, provider.TypeCertificate} {
|
||||
for _, o := range m.cache.objects[containerKey(c)+"|"+string(t)].value {
|
||||
for _, o := range m.cache.objects[cachefile.ObjectsKey(c, t)].value {
|
||||
r, err := ref.ForObject(o, "")
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -166,7 +167,7 @@ func (m *Model) coverageOf(in providerFilter) coverage {
|
||||
}
|
||||
for _, v := range e.value {
|
||||
c.vaults++
|
||||
key := containerKey(v) + "|" + string(provider.TypeSecret)
|
||||
key := cachefile.ObjectsKey(v, provider.TypeSecret)
|
||||
if objs, ok := m.cache.objects[key]; ok {
|
||||
c.vaultsLoaded++
|
||||
c.secrets += len(objs.value)
|
||||
@@ -263,7 +264,7 @@ func (m *Model) queueSource(src provider.Source, force bool) {
|
||||
// queueContainer queues loading c's secrets, unless loaded lately and not
|
||||
// force.
|
||||
func (m *Model) queueContainer(c provider.Container, row string, force bool) {
|
||||
key := containerKey(c) + "|" + string(provider.TypeSecret)
|
||||
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
||||
if e, ok := m.cache.objects[key]; ok && e.fresh(m.now()) && !force {
|
||||
return
|
||||
}
|
||||
@@ -327,7 +328,7 @@ func (m *Model) handleCrawl(msg crawlMsg) tea.Cmd {
|
||||
m.crawl.active--
|
||||
key := msg.job.row
|
||||
if msg.job.container != nil {
|
||||
key = containerKey(*msg.job.container) + "|" + string(provider.TypeSecret)
|
||||
key = cachefile.ObjectsKey(*msg.job.container, provider.TypeSecret)
|
||||
}
|
||||
delete(m.crawl.pending, key)
|
||||
switch {
|
||||
@@ -365,7 +366,7 @@ func (m *Model) sourceMark(row string) string {
|
||||
}
|
||||
loaded, loading := 0, false
|
||||
for _, c := range e.value {
|
||||
key := containerKey(c) + "|" + string(provider.TypeSecret)
|
||||
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
||||
loading = loading || m.crawl.pending[key]
|
||||
if _, ok := m.cache.objects[key]; ok || m.crawl.failed[key] {
|
||||
loaded++
|
||||
@@ -386,7 +387,7 @@ func (m *Model) sourceMark(row string) string {
|
||||
// (✗) or are loading (◌): whether a subscription is loaded says whether
|
||||
// its vaults are searchable.
|
||||
func (m *Model) vaultMark(c provider.Container) string {
|
||||
key := containerKey(c) + "|" + string(provider.TypeSecret)
|
||||
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
||||
switch {
|
||||
case m.crawl.pending[key]:
|
||||
return m.st.Muted.Render("◌")
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/charmbracelet/x/ansi"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/match"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
@@ -617,7 +618,7 @@ func (m Model) loadHint() string {
|
||||
}
|
||||
loaded := 0
|
||||
for _, c := range e.value {
|
||||
key := containerKey(c) + "|" + string(provider.TypeSecret)
|
||||
key := cachefile.ObjectsKey(c, provider.TypeSecret)
|
||||
if _, ok := m.cache.objects[key]; ok || m.crawl.failed[key] {
|
||||
loaded++
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
@@ -80,7 +81,7 @@ func newSourcesList(iconSet icons.Set, mem *store.Store, recent bool) panes.List
|
||||
kind := st.Kind(r.group)
|
||||
starred := ""
|
||||
if r.kind == rowSource {
|
||||
starred = marks(st, node, r.source.Provider, recent)
|
||||
starred = marks(st, node, r.source.Provider, recent, time.Now())
|
||||
}
|
||||
switch {
|
||||
case r.kind == rowLinks:
|
||||
|
||||
Reference in new issue
Block a user