feat: l for details; details for vaults, scopes and files; getting started; the age of cached lists
#17
No files matched your search
@@ -1,11 +1,13 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"maps"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -21,8 +23,12 @@ import (
|
|||||||
// dates come with the lists, so it's one request per vault and type,
|
// dates come with the lists, so it's one request per vault and type,
|
||||||
// a few at a time. Only Key Vault has expiry dates.
|
// a few at a time. Only Key Vault has expiry dates.
|
||||||
|
|
||||||
// errExpiring makes the exit status 1 when anything was listed.
|
// The exit status is grep's: 1 when anything was listed (errExpiring),
|
||||||
var errExpiring = errors.New("expiring")
|
// else 2 when some lists couldn't be read (errIncomplete), else 0.
|
||||||
|
var (
|
||||||
|
errExpiring = errors.New("expiring")
|
||||||
|
errIncomplete = errors.New("incomplete")
|
||||||
|
)
|
||||||
|
|
||||||
// listLimit is how many lists load at once.
|
// listLimit is how many lists load at once.
|
||||||
const listLimit = 8
|
const listLimit = 8
|
||||||
@@ -55,11 +61,14 @@ func expiring(providers []provider.Provider, o expiringOpts, out, errs io.Writer
|
|||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
limit = make(chan struct{}, listLimit)
|
limit = make(chan struct{}, listLimit)
|
||||||
within = o.now.AddDate(0, 0, o.days)
|
within = o.now.AddDate(0, 0, o.days)
|
||||||
|
// A vault's lists that couldn't be read, said once per vault.
|
||||||
|
denied = map[string][]string{}
|
||||||
|
why = map[string]string{}
|
||||||
warnErr = func(what string, err error) {
|
warnErr = func(what string, err error) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
failed++
|
failed++
|
||||||
fmt.Fprintf(errs, "lazysecrets: %s: %v\n", what, err)
|
fmt.Fprintf(errs, "lazysecrets: %s: %s\n", what, briefly(err))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
run := func(f func(ctx context.Context)) {
|
run := func(f func(ctx context.Context)) {
|
||||||
@@ -109,12 +118,14 @@ func expiring(providers []provider.Provider, o expiringOpts, out, errs io.Writer
|
|||||||
for _, t := range c.ObjectTypes() {
|
for _, t := range c.ObjectTypes() {
|
||||||
run(func(ctx context.Context) {
|
run(func(ctx context.Context) {
|
||||||
objs, err := p.Objects(ctx, c, t)
|
objs, err := p.Objects(ctx, c, t)
|
||||||
if err != nil {
|
|
||||||
warnErr(c.Name+" "+strings.ToLower(string(t))+"s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
if err != nil {
|
||||||
|
failed++
|
||||||
|
denied[c.Name] = append(denied[c.Name], strings.ToLower(string(t))+"s")
|
||||||
|
why[c.Name] = cmp.Or(why[c.Name], briefly(err))
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, o := range objs {
|
for _, o := range objs {
|
||||||
if !o.Expires.IsZero() && o.Expires.Before(within) {
|
if !o.Expires.IsZero() && o.Expires.Before(within) {
|
||||||
found = append(found, o)
|
found = append(found, o)
|
||||||
@@ -130,12 +141,20 @@ func expiring(providers []provider.Provider, o expiringOpts, out, errs io.Writer
|
|||||||
// Listing vaults queues the lists of their secrets, so wait for both.
|
// Listing vaults queues the lists of their secrets, so wait for both.
|
||||||
vaultsDone.Wait()
|
vaultsDone.Wait()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
for _, name := range slices.Sorted(maps.Keys(denied)) {
|
||||||
|
fmt.Fprintf(errs, "lazysecrets: %s: can't list %s: %s\n", name, strings.Join(denied[name], ", "), why[name])
|
||||||
|
}
|
||||||
|
|
||||||
slices.SortFunc(found, func(a, b provider.Object) int { return a.Expires.Compare(b.Expires) })
|
slices.SortFunc(found, func(a, b provider.Object) int { return a.Expires.Compare(b.Expires) })
|
||||||
if len(found) == 0 {
|
if len(found) == 0 {
|
||||||
fmt.Fprintf(errs, "nothing expires within %d days (%d vaults)\n", o.days, vaults)
|
vault := "vaults"
|
||||||
|
if vaults == 1 {
|
||||||
|
vault = "vault"
|
||||||
|
}
|
||||||
|
fmt.Fprintf(errs, "nothing expires within %d days (checked %d of %d %s)\n", o.days, vaults-len(denied), vaults, vault)
|
||||||
if failed > 0 {
|
if failed > 0 {
|
||||||
return fmt.Errorf("%d lists couldn't be read", failed)
|
fmt.Fprintf(errs, "lazysecrets: %d lists couldn't be read: they weren't checked\n", failed)
|
||||||
|
return errIncomplete
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -161,3 +180,13 @@ func expiring(providers []provider.Provider, o expiringOpts, out, errs io.Writer
|
|||||||
}
|
}
|
||||||
return errExpiring
|
return errExpiring
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// briefly is err on one line: for a sign-in, the command that signs in.
|
||||||
|
func briefly(err error) string {
|
||||||
|
var auth *provider.AuthError
|
||||||
|
if errors.As(err, &auth) && len(auth.Login) > 0 {
|
||||||
|
return "sign-in required: " + strings.Join(auth.Login, " ")
|
||||||
|
}
|
||||||
|
line, _, _ := strings.Cut(err.Error(), "\n")
|
||||||
|
return line
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ func TestExpiring(t *testing.T) {
|
|||||||
!strings.Contains(lines[1], "lazysecrets://azure/kv-app-prod.vault.azure.net/certificates/old-tls") {
|
!strings.Contains(lines[1], "lazysecrets://azure/kv-app-prod.vault.azure.net/certificates/old-tls") {
|
||||||
t.Errorf("out:\n%s", out.String())
|
t.Errorf("out:\n%s", out.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(errs.String(), "kv-restricted") {
|
if !strings.Contains(errs.String(), "kv-restricted: can't list ") || strings.Count(errs.String(), "kv-restricted") != 1 {
|
||||||
t.Errorf("unreadable vaults not said:\n%s", errs.String())
|
t.Errorf("unreadable vaults not said:\n%s", errs.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,6 +35,13 @@ func TestExpiring(t *testing.T) {
|
|||||||
t.Errorf("400 days:\n%s", out.String())
|
t.Errorf("400 days:\n%s", out.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Nothing listed, but not everything checked.
|
||||||
|
errs.Reset()
|
||||||
|
err = expiring(providers, expiringOpts{days: 0, timeout: time.Minute, now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)}, &out, &errs)
|
||||||
|
if !errors.Is(err, errIncomplete) {
|
||||||
|
t.Errorf("err %v, want errIncomplete", err)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := parseExpiring([]string{"--days", "-1"}, 30); err == nil {
|
if _, err := parseExpiring([]string{"--days", "-1"}, 30); err == nil {
|
||||||
t.Error("negative days accepted")
|
t.Error("negative days accepted")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,9 +34,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(); errors.Is(err, errExpiring) {
|
switch err := run(); {
|
||||||
|
case errors.Is(err, errExpiring):
|
||||||
os.Exit(1) // listed: a pipeline fails on it
|
os.Exit(1) // listed: a pipeline fails on it
|
||||||
} else if err != nil {
|
case errors.Is(err, errIncomplete):
|
||||||
|
os.Exit(2)
|
||||||
|
case err != nil:
|
||||||
fmt.Fprintln(os.Stderr, "lazysecrets:", err)
|
fmt.Fprintln(os.Stderr, "lazysecrets:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -123,7 +126,8 @@ func run() error {
|
|||||||
for scripts: VAR=$(lazysecrets get <ref>)
|
for scripts: VAR=$(lazysecrets get <ref>)
|
||||||
lazysecrets [flags] expiring [--days N]
|
lazysecrets [flags] expiring [--days N]
|
||||||
list what has expired or will within N days
|
list what has expired or will within N days
|
||||||
(default 30) in every vault; exits 1 if any
|
(default 30) in every vault; exits 1 if any,
|
||||||
|
2 if it couldn't check everything
|
||||||
lazysecrets [flags] keybindings print every action and its keys (with your
|
lazysecrets [flags] keybindings print every action and its keys (with your
|
||||||
config applied), as config to copy and edit
|
config applied), as config to copy and edit
|
||||||
lazysecrets handler install [--terminal NAME]
|
lazysecrets handler install [--terminal NAME]
|
||||||
@@ -310,7 +314,8 @@ Flags:
|
|||||||
if expiringArgs != nil {
|
if expiringArgs != nil {
|
||||||
days, err := parseExpiring(expiringArgs, expiryDays)
|
days, err := parseExpiring(expiringArgs, expiryDays)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
fmt.Fprintln(os.Stderr, "lazysecrets:", err)
|
||||||
|
return errIncomplete // 2, not 1: nothing was checked
|
||||||
}
|
}
|
||||||
return expiring(providers, expiringOpts{days: days, timeout: *timeout, now: time.Now()}, os.Stdout, os.Stderr)
|
return expiring(providers, expiringOpts{days: days, timeout: *timeout, now: time.Now()}, os.Stdout, os.Stderr)
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -72,8 +72,7 @@ with the lists. It says how many vaults it covers; `ctrl+a` in search
|
|||||||
loads the rest.
|
loads the rest.
|
||||||
|
|
||||||
For a pipeline or a cron job, `lazysecrets expiring` lists them in every
|
For a pipeline or a cron job, `lazysecrets expiring` lists them in every
|
||||||
vault of every account, loading each vault's lists (a few at a time), and
|
vault of every account, loading each vault's lists (a few at a time):
|
||||||
exits 1 when it lists anything:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ lazysecrets expiring --days 30
|
$ lazysecrets expiring --days 30
|
||||||
@@ -82,8 +81,10 @@ expired 2026-03-01 certificate kv-app-prod old-tls lazysecrets://azure/kv-a
|
|||||||
12d 2026-10-23 secret kv-app-prod api-key lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/api-key
|
12d 2026-10-23 secret kv-app-prod api-key lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/api-key
|
||||||
```
|
```
|
||||||
|
|
||||||
`--days` defaults to `expiryWarningDays`. Lists it can't read are named on
|
`--days` defaults to `expiryWarningDays`. Accounts that need a sign-in and
|
||||||
stderr.
|
lists it can't read are named on stderr, one line each. Like grep, it
|
||||||
|
exits 0 when nothing expires, 1 when it lists something, and 2 when it
|
||||||
|
couldn't check everything.
|
||||||
|
|
||||||
On a vault, the right pane shows its settings: resource group, location,
|
On a vault, the right pane shows its settings: resource group, location,
|
||||||
SKU, whether access is by Azure roles or access policies, soft delete and
|
SKU, whether access is by Azure roles or access policies, soft delete and
|
||||||
|
|||||||
Reference in new issue
Block a user