159 lines
5.4 KiB
Go
159 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/signal"
|
|
"slices"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"charm.land/lipgloss/v2"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/cachefile"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/listing"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/icons"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
|
)
|
|
|
|
// listCmd prints every source, vault and secret lazysecrets can find, one
|
|
// per line, for fzf, editor pickers and scripts:
|
|
//
|
|
// lazysecrets list | fzf --ansi --delimiter='\t' --with-nth=1 | cut -f2
|
|
//
|
|
// Each line is the name, then the link: the name coloured from the theme,
|
|
// with the provider's icon (as in the TUI: --icons, LAZYSECRETS_ICONS,
|
|
// ui.icons); the link never is.
|
|
//
|
|
// What's cached prints at once; what's missing or older than --max-age
|
|
// follows as it loads. A list that can't be read is a line too, as it
|
|
// fails: the vault or source, then ✗ and why in the theme's error colour.
|
|
// They're in the output rather than on stderr, which would draw over fzf.
|
|
func listCmd(providers []provider.Provider, cachePath string, timeout func(provider.Kind) time.Duration, theme styles.Styles, withIcons bool, args []string, out io.Writer) error {
|
|
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: name 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)")
|
|
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
|
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if fs.NArg() > 0 {
|
|
return fmt.Errorf("usage: lazysecrets list [--kind secret,…] [--cached] [--max-age 2m] [--format text|json]")
|
|
}
|
|
var only []string
|
|
for k := range strings.SplitSeq(*kinds, ",") {
|
|
if k = strings.TrimSpace(k); k == "" {
|
|
continue
|
|
}
|
|
if !slices.Contains(listing.Kinds, k) {
|
|
return fmt.Errorf("--kind: %q is not one of %s", k, strings.Join(listing.Kinds, ", "))
|
|
}
|
|
only = append(only, k)
|
|
}
|
|
if *color != "always" && *color != "never" {
|
|
return fmt.Errorf("--color: %q is not always or never", *color)
|
|
}
|
|
iconSet := icons.None
|
|
if *iconsOn {
|
|
iconSet = icons.NerdFont
|
|
}
|
|
var write func(listing.Entry) error
|
|
switch *format {
|
|
case "text":
|
|
write = func(e listing.Entry) error {
|
|
// A tab or line break in a name would break the line up.
|
|
_, err := fmt.Fprintf(out, "%s\t%s\n", oneLine.Replace(nameOf(e, theme, iconSet, *color == "always")), e.Link)
|
|
return err
|
|
}
|
|
case "json":
|
|
enc := json.NewEncoder(out)
|
|
enc.SetEscapeHTML(false)
|
|
write = func(e listing.Entry) error { return enc.Encode(e) }
|
|
default:
|
|
return fmt.Errorf("--format: %q is not text or json", *format)
|
|
}
|
|
|
|
// When the reader goes away (a pick in fzf), writing fails instead of
|
|
// killing the process, so what loaded is still saved.
|
|
signal.Notify(make(chan os.Signal, 1), syscall.SIGPIPE)
|
|
var gone error
|
|
emit := func(e listing.Entry) error {
|
|
if e.Error != "" && !*showErrors {
|
|
return nil
|
|
}
|
|
if err := write(e); err != nil {
|
|
gone = err
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
f := cachefile.New()
|
|
if cachePath != "" {
|
|
f = cachefile.Read(cachePath)
|
|
}
|
|
f = listing.Run(context.Background(), listing.Options{
|
|
Providers: providers, Cache: f, MaxAge: *maxAge, Cached: *cached, Kinds: only, Timeout: timeout,
|
|
}, emit)
|
|
if cachePath != "" && !*cached {
|
|
byID := map[string]provider.Provider{}
|
|
for _, p := range providers {
|
|
byID[p.ID()] = p
|
|
}
|
|
keep := func(id string) bool { p, ok := byID[id]; return ok && p.Kind() != provider.KindRepo }
|
|
if err := cachefile.Save(cachePath, f, keep, time.Now()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if gone != nil && !errors.Is(gone, syscall.EPIPE) {
|
|
return gone
|
|
}
|
|
return nil // or the reader has what it wanted
|
|
}
|
|
|
|
var oneLine = strings.NewReplacer("\t", " ", "\r\n", " ", "\n", " ", "\r", " ")
|
|
|
|
// nameOf is an entry's name for the text format: the provider's icon,
|
|
// the path with the item's own name standing out, and the account. A
|
|
// list that couldn't be read has ✗ and why after it.
|
|
func nameOf(e listing.Entry, theme styles.Styles, iconSet icons.Set, color bool) string {
|
|
render := func(st lipgloss.Style, s string) string {
|
|
if !color || s == "" {
|
|
return s
|
|
}
|
|
return st.Render(s)
|
|
}
|
|
kind := provider.Kind("")
|
|
for _, k := range []provider.Kind{provider.KindAzure, provider.KindDatabricks, provider.KindRepo} {
|
|
if strings.EqualFold(string(k), e.Provider) {
|
|
kind = k
|
|
}
|
|
}
|
|
icon := iconSet.Prefix(kind)
|
|
var name string
|
|
if n := len(e.Path); n > 0 {
|
|
if n > 1 {
|
|
name = render(theme.Muted, strings.Join(e.Path[:n-1], " / ")+" / ")
|
|
}
|
|
name += e.Path[n-1]
|
|
}
|
|
if e.Account != "" {
|
|
name += render(theme.Muted, " · "+e.Account)
|
|
}
|
|
if e.Error != "" {
|
|
name += " " + render(theme.Error, "✗ "+e.Error)
|
|
}
|
|
return render(theme.Kind(kind), strings.TrimSuffix(icon, " ")) + strings.Repeat(" ", min(len(icon), 1)) + name
|
|
}
|