feat: what's expiring, in the status pane and lazysecrets expiring
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9b0329e9da
commit
c9bc4c1431
10 files changed
+364
-3
No files matched your search
@@ -841,6 +841,12 @@ func (m *Model) handleKey(msg tea.KeyPressMsg) (cmd tea.Cmd, quit bool) {
|
||||
m.focus = paneDetail // its rows are on the right
|
||||
case m.focus == paneSources && (key.Matches(msg, k.Enter) || key.Matches(msg, k.Reveal)) && m.toggleGroup():
|
||||
// folded or unfolded a provider group
|
||||
case m.focus == paneDetail && key.Matches(msg, k.Enter) && m.fieldLink() != "":
|
||||
r, err := ref.Parse(m.fieldLink())
|
||||
if err == nil {
|
||||
f, _ := m.fields.Selected()
|
||||
m.startNav(r, actGo, f.Row)
|
||||
}
|
||||
case key.Matches(msg, k.Enter):
|
||||
if m.focus < paneDetail {
|
||||
m.focus++
|
||||
@@ -1738,6 +1744,14 @@ func (m Model) problemText(providerID, err string) string {
|
||||
return err
|
||||
}
|
||||
|
||||
// fieldLink is the link of the selected Fields row, if it has one.
|
||||
func (m Model) fieldLink() string {
|
||||
if f, ok := m.fields.Selected(); ok && m.fields.Shown() {
|
||||
return f.Link
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// loginFor is the sign-in L runs: the selected account's, if it needs
|
||||
// one, else the last one asked for.
|
||||
func (m Model) loginFor() *provider.AuthError {
|
||||
|
||||
@@ -1236,3 +1236,25 @@ func TestStatusPane(t *testing.T) {
|
||||
t.Errorf("back to %v", d.m.focus)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpiringInStatus(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{ExpiryWarning: 30 * 24 * time.Hour}, fake.Azure(0))
|
||||
d.keys("3", "]", "]") // load kv-app-prod's certificates
|
||||
if !strings.Contains(d.screen(), "⚠ 1 expiring") {
|
||||
t.Errorf("status line:\n%s", d.screen())
|
||||
}
|
||||
d.keys("0")
|
||||
scr := d.screen()
|
||||
for _, want := range []string{"expiring ✗ expired old-tls · kv-app-prod", "in 1 of 3 vaults (ctrl+a in search loads all)"} {
|
||||
if !strings.Contains(scr, want) {
|
||||
t.Errorf("missing %q:\n%s", want, scr)
|
||||
}
|
||||
}
|
||||
d.keys("3", "[", "[", "0", "enter") // elsewhere, then to it from the status pane
|
||||
d.m.fields.SelectWhere(func(f panes.Field) bool { return f.Label == "expiring" })
|
||||
d.keys("enter")
|
||||
if _, v, o := d.selected(); v != "kv-app-prod" || o != "old-tls" {
|
||||
t.Errorf("enter went to %s/%s\n%s", v, o, d.screen())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/panes"
|
||||
)
|
||||
|
||||
// The status pane lists what has expired or will soon, across every vault
|
||||
// loaded (opened, searched, or from the cache): expiry dates come with
|
||||
// the lists, so this makes no requests. enter on one goes to it.
|
||||
|
||||
type expiringItem struct {
|
||||
o provider.Object
|
||||
row string // its source's row key, to go to it
|
||||
}
|
||||
|
||||
// expiringItems are the loaded objects that have expired or will within
|
||||
// the warning period, soonest first.
|
||||
func (m Model) expiringItems() []expiringItem {
|
||||
var out []expiringItem
|
||||
for _, p := range m.providers {
|
||||
for _, src := range m.sourceResults[p.ID()].sources {
|
||||
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 {
|
||||
if state, _ := m.expiry(o); state != notExpiring {
|
||||
out = append(out, expiringItem{o, row})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.SortFunc(out, func(a, b expiringItem) int {
|
||||
return cmp.Or(a.o.Expires.Compare(b.o.Expires), cmp.Compare(a.o.Name, b.o.Name))
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
// expiringFields are the status pane's rows for them, and how much of
|
||||
// everything they cover.
|
||||
func (m Model) expiringFields() []panes.Field {
|
||||
items := m.expiringItems()
|
||||
var out []panes.Field
|
||||
for _, it := range items {
|
||||
state, left := m.expiry(it.o)
|
||||
mark := "⚠ " + daysLeft(left)
|
||||
if state == expired {
|
||||
mark = "✗ expired"
|
||||
}
|
||||
f := panes.Field{Value: mark + " " + it.o.Name + " · " + it.o.Container.Name, Err: state == expired, Row: it.row}
|
||||
if r, err := ref.ForObject(it.o, ""); err == nil {
|
||||
f.Link = r.String()
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
cov := m.coverageOf(provider.KindAzure)
|
||||
switch {
|
||||
case cov.vaults == 0 && len(items) == 0:
|
||||
return nil
|
||||
case !cov.complete():
|
||||
out = append(out, panes.Field{Value: fmt.Sprintf("in %d of %d vaults", cov.vaultsLoaded, cov.vaults) +
|
||||
withKey(m.keys.SearchLoadAll, " (%s in search loads all)"), Info: true})
|
||||
case len(items) == 0:
|
||||
out = append(out, panes.Field{Value: fmt.Sprintf("nothing within %d days", int(m.expiryWarning.Hours()/24)), Info: true})
|
||||
}
|
||||
out[0].Label = "expiring"
|
||||
return out
|
||||
}
|
||||
@@ -38,6 +38,9 @@ func (m Model) statusLine() string {
|
||||
failing++
|
||||
}
|
||||
}
|
||||
if n := len(m.expiringItems()); n > 0 {
|
||||
parts = append(parts, m.st.Warning.Render(fmt.Sprintf("⚠ %d expiring", n)))
|
||||
}
|
||||
if accounts > 0 {
|
||||
s := plural(accounts, "account")
|
||||
if failing > 0 {
|
||||
@@ -88,6 +91,8 @@ func (m Model) statusFields() []panes.Field {
|
||||
add(f)
|
||||
}
|
||||
|
||||
out = append(out, m.expiringFields()...)
|
||||
|
||||
for i, path := range m.configFiles {
|
||||
label := ""
|
||||
if i == 0 {
|
||||
|
||||
@@ -20,12 +20,15 @@ type Fields struct {
|
||||
|
||||
// Field is a row: a value under a label. Info rows (loading, errors)
|
||||
// can't be copied; Err ones are errors. A row can also be opened: URL in
|
||||
// the browser (o), Path in the editor (e), or Provider signed in to (L).
|
||||
// the browser (o), Path in the editor (e), Provider signed in to (L), or
|
||||
// Link gone to (enter).
|
||||
type Field struct {
|
||||
Label, Value string
|
||||
Info, Err bool
|
||||
|
||||
URL, Path, Provider string
|
||||
// Link is a lazysecrets link enter goes to, in the source of row Row.
|
||||
Link, Row string
|
||||
}
|
||||
|
||||
func NewFields() Fields {
|
||||
|
||||
Reference in new issue
Block a user