commit
edf6e79327
134 files changed
+27239
No files matched your search
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/term"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||
)
|
||||
|
||||
// get prints the value a link points at, for scripts:
|
||||
//
|
||||
// export DB_PASSWORD=$(lazysecrets get lazysecrets://azure/kv.vault.azure.net/secrets/db-password)
|
||||
//
|
||||
// It ends with a newline only when printing to a terminal, so command
|
||||
// substitution and pipes get the value exactly.
|
||||
func get(ctx context.Context, providers []provider.Provider, st *store.Store, r ref.Ref) error {
|
||||
v, err := value(ctx, providers, st, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out := v.Reveal()
|
||||
if term.IsTerminal(int(os.Stdout.Fd())) {
|
||||
out += "\n"
|
||||
}
|
||||
_, err = os.Stdout.WriteString(out)
|
||||
return err
|
||||
}
|
||||
|
||||
// value reads what r points at without listing its vault: the vault and
|
||||
// name are enough for Azure, tried with each account (the one that last
|
||||
// read it first); for Databricks the workspace's profile is found by its
|
||||
// host.
|
||||
func value(ctx context.Context, providers []provider.Provider, st *store.Store, r ref.Ref) (provider.SecretValue, error) {
|
||||
if r.IsSource() || r.IsContainer() {
|
||||
return provider.SecretValue{}, fmt.Errorf("%s points at a %s, not a secret", r, map[bool]string{true: "subscription or workspace", false: "vault or scope"}[r.IsSource()])
|
||||
}
|
||||
if r.Kind != provider.KindAzure && r.Host == "" {
|
||||
return provider.SecretValue{}, errors.New("{{secrets/…}} names no workspace: use its lazysecrets:// link (s in lazysecrets)")
|
||||
}
|
||||
key := r
|
||||
key.Version = ""
|
||||
node := st.Node(key.String())
|
||||
|
||||
type try struct {
|
||||
p provider.Provider
|
||||
o provider.Object
|
||||
}
|
||||
var tries []try
|
||||
for _, p := range providers {
|
||||
if p.Kind() != r.Kind {
|
||||
continue
|
||||
}
|
||||
o := provider.Object{Name: r.Name, Type: cmp.Or(r.Type, provider.TypeSecret), Version: r.Version}
|
||||
if r.Kind != provider.KindAzure { // by host: workspaces, repos
|
||||
loc, ok := p.(provider.Locator)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
profile, found, err := loc.Locate(ctx, r.Host)
|
||||
if err != nil || !found {
|
||||
continue
|
||||
}
|
||||
src := provider.Source{Kind: r.Kind, Provider: p.ID(), ID: profile}
|
||||
o.Container = provider.Container{Source: src, Name: r.Scope}
|
||||
// Listing the scopes tells a Key Vault-backed one, whose value
|
||||
// can then be read from the vault if Databricks refuses.
|
||||
if cs, err := p.Containers(ctx, src); err == nil {
|
||||
for _, c := range cs {
|
||||
if c.Name == r.Scope {
|
||||
c.Source = src
|
||||
o.Container = c
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
src := provider.Source{Kind: r.Kind, Provider: p.ID(), ID: node.Source}
|
||||
o.Container = provider.Container{Source: src, Name: r.ContainerName(), Detail: r.VaultURL()}
|
||||
}
|
||||
t := try{p, o}
|
||||
if p.ID() == node.Account {
|
||||
tries = append([]try{t}, tries...)
|
||||
} else {
|
||||
tries = append(tries, t)
|
||||
}
|
||||
}
|
||||
if len(tries) == 0 {
|
||||
switch r.Kind {
|
||||
case provider.KindDatabricks:
|
||||
return provider.SecretValue{}, fmt.Errorf("no Databricks profile for workspace %s", r.Host)
|
||||
case provider.KindRepo:
|
||||
return provider.SecretValue{}, fmt.Errorf("repo %s isn't here: run it inside the repo, or list it under providers.repos.paths", r.Host)
|
||||
}
|
||||
return provider.SecretValue{}, fmt.Errorf("no %s account to read %s with", r.Kind, r)
|
||||
}
|
||||
var errs []error
|
||||
for _, t := range tries {
|
||||
v, err := t.p.Value(ctx, t.o)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if len(tries) == 1 {
|
||||
return provider.SecretValue{}, err
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("%s: %w", t.p.Label(), err))
|
||||
}
|
||||
return provider.SecretValue{}, errors.Join(errs...)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||
)
|
||||
|
||||
func TestValue(t *testing.T) {
|
||||
providers := []provider.Provider{fake.Azure(0), fake.Databricks(0)}
|
||||
st, _ := store.Load("")
|
||||
for link, want := range map[string]string{
|
||||
"lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password": "correct-horse-battery-staple",
|
||||
"lazysecrets://azure/kv-app-prod.vault.azure.net/secrets/db-password/0a77c4": "original-password",
|
||||
"lazysecrets://databricks/adb-1111111111111111.11.azuredatabricks.net/app/storage-key": "storage-demo-key",
|
||||
} {
|
||||
r, _ := ref.Parse(link)
|
||||
v, err := value(context.Background(), providers, st, r)
|
||||
if err != nil || v.Reveal() != want {
|
||||
t.Errorf("%s: %q, %v", link, v.Reveal(), err)
|
||||
}
|
||||
}
|
||||
for link, wantErr := range map[string]string{
|
||||
"lazysecrets://azure/kv-app-prod.vault.azure.net": "not a secret",
|
||||
"lazysecrets://databricks/adb-9.9.azuredatabricks.net/app/storage-key": "no Databricks profile",
|
||||
"{{secrets/app/storage-key}}": "names no workspace",
|
||||
} {
|
||||
r, _ := ref.Parse(link)
|
||||
if _, err := value(context.Background(), providers, st, r); err == nil || !strings.Contains(err.Error(), wantErr) {
|
||||
t.Errorf("%s: %v, want %q", link, err, wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarredRepos(t *testing.T) {
|
||||
st, _ := store.Load("")
|
||||
st.Update("lazysecrets://repo/git.example.com%2Fa/secrets%2Fx.yaml/db.password", func(n *store.Node) {
|
||||
n.Source, n.StarredBy = "/repos/a", []string{"Repos"}
|
||||
})
|
||||
st.Update("lazysecrets://repo/git.example.com%2Fb", func(n *store.Node) { n.Source, n.Note = "/repos/b", "noted, not starred" })
|
||||
st.Update("lazysecrets://azure/kv.vault.azure.net", func(n *store.Node) { n.Source, n.StarredBy = "sub", []string{"Azure"} })
|
||||
if got := starredRepos(st); len(got) != 1 || got[0] != "/repos/a" {
|
||||
t.Errorf("starred repos %v", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/config"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/instance"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/linkhandler"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/app"
|
||||
)
|
||||
|
||||
// handler installs or removes the lazysecrets:// link handler.
|
||||
func handler(configPath string, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return fmt.Errorf("usage: lazysecrets handler install [--terminal NAME] | uninstall")
|
||||
}
|
||||
switch args[0] {
|
||||
case "install":
|
||||
fs := flag.NewFlagSet("handler install", flag.ContinueOnError)
|
||||
term := fs.String("terminal", "", "terminal to open links in: "+strings.Join(linkhandler.Terminals, ", ")+
|
||||
" (default: the one this runs in)")
|
||||
if err := fs.Parse(args[1:]); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.LoadAll(strings.Split(configPath, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
where, opens, err := installHandler(cfg, configPath, *term)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("lazysecrets:// links now open in a new %s window.\nInstalled %s\n", opens, where)
|
||||
if runtime.GOOS == "darwin" {
|
||||
fmt.Println("Try it: open 'lazysecrets://azure/<vault>.vault.azure.net'")
|
||||
}
|
||||
return nil
|
||||
case "uninstall":
|
||||
where, err := linkhandler.Uninstall()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Removed %s; lazysecrets:// links no longer open lazysecrets.\n", where)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("unknown handler command %q: install or uninstall", args[0])
|
||||
}
|
||||
|
||||
// installHandler registers this lazysecrets for lazysecrets:// links,
|
||||
// opening them in term (default: the terminal it runs in). It returns
|
||||
// where it installed and what links open in.
|
||||
func installHandler(cfg config.Config, configPath, term string) (string, string, error) {
|
||||
exe, err := linkhandler.Executable()
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if term == "" {
|
||||
term = linkhandler.Detect(os.Getenv)
|
||||
}
|
||||
// Check now that a link could be opened, rather than on the first
|
||||
// click.
|
||||
if _, err := launchCommand(cfg, configPath, term, exe, "lazysecrets://check"); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
launch := []string{exe}
|
||||
if configPath != defaultConfigPath() {
|
||||
launch = append(launch, "--config", configPath)
|
||||
}
|
||||
launch = append(launch, "launch")
|
||||
if term != "" {
|
||||
launch = append(launch, "--terminal", term)
|
||||
}
|
||||
where, err := linkhandler.Install(launch, linkhandler.Env(os.Getenv))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
opens := term
|
||||
if len(cfg.LinkHandler.Command) > 0 {
|
||||
opens = strings.Join(cfg.LinkHandler.Command, " ")
|
||||
} else if cfg.LinkHandler.Terminal != "" {
|
||||
opens = cfg.LinkHandler.Terminal
|
||||
}
|
||||
return where, opens, nil
|
||||
}
|
||||
|
||||
// handlerSetup offers to set up the link handler at startup, if links
|
||||
// don't open lazysecrets and it can be: not for a go run binary, which is
|
||||
// deleted on exit, nor when the config says not to ask.
|
||||
func handlerSetup(cfg config.Config, configPath string) *app.HandlerSetup {
|
||||
if !config.On(cfg.LinkHandler.Prompt) {
|
||||
return nil
|
||||
}
|
||||
if _, err := linkhandler.Executable(); err != nil {
|
||||
return nil
|
||||
}
|
||||
reason := linkhandler.Check()
|
||||
if reason == "" {
|
||||
return nil
|
||||
}
|
||||
return &app.HandlerSetup{
|
||||
Reason: reason,
|
||||
Install: func() (string, error) {
|
||||
_, opens, err := installHandler(cfg, configPath, "")
|
||||
return opens, err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// launch opens link in lazysecrets in a new terminal window. The
|
||||
// operating system runs it when a lazysecrets:// link is opened.
|
||||
func launch(configPath string, args []string) error {
|
||||
fs := flag.NewFlagSet("launch", flag.ContinueOnError)
|
||||
term := fs.String("terminal", "", "terminal to open the window in")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.NArg() != 1 {
|
||||
return fmt.Errorf("usage: lazysecrets launch [--terminal NAME] <link>")
|
||||
}
|
||||
link := fs.Arg(0)
|
||||
// Links can come from any web page: open only what lazysecrets can
|
||||
// read. Opening never acts by itself; the user picks what to do.
|
||||
if strings.ContainsAny(link, "\r\n") {
|
||||
return fmt.Errorf("not a link: %q", link)
|
||||
}
|
||||
if _, err := ref.Parse(link); err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := config.LoadAll(strings.Split(configPath, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Where the link was clicked: a terminal gets it there, anywhere else
|
||||
// the lazysecrets used last does.
|
||||
front := linkhandler.FrontApp()
|
||||
inTerminal := linkhandler.IsTerminal(front.Bundle)
|
||||
if config.On(cfg.LinkHandler.Reuse) && handOver(link, front.Bundle, inTerminal) {
|
||||
return nil
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
argv, err := launchCommand(cfg, configPath, *term, exe, link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return linkhandler.Launch(argv)
|
||||
}
|
||||
|
||||
// handOver gives link to the running lazysecrets last used that can be
|
||||
// brought into view, if there is one; clicked in a terminal, only to one
|
||||
// in that terminal.
|
||||
func handOver(link, front string, inTerminal bool) bool {
|
||||
stateDir, err := config.StateDir()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
best, ok := instance.Best(instance.Running(ctx, instance.Dir(stateDir), front), front, inTerminal)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// A fresh deadline: the hand-over must not fail for time the status
|
||||
// round used, or the link would open twice.
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
return instance.Open(ctx, best, link, front) == nil
|
||||
}
|
||||
|
||||
// launchCommand is the command opening a new window of the configured
|
||||
// terminal (or term) running lazysecrets on link.
|
||||
func launchCommand(cfg config.Config, configPath, term, exe, link string) ([]string, error) {
|
||||
if cfg.LinkHandler.Terminal != "" {
|
||||
term = cfg.LinkHandler.Terminal
|
||||
}
|
||||
return linkhandler.Command(runtime.GOOS, term, cfg.LinkHandler.Command, innerCommand(configPath, exe, link))
|
||||
}
|
||||
|
||||
// innerCommand runs lazysecrets on link, with the environment it needs.
|
||||
func innerCommand(configPath, exe, link string) []string {
|
||||
inner := append(append([]string{"/usr/bin/env"}, linkhandler.Env(os.Getenv)...), exe)
|
||||
if configPath != defaultConfigPath() {
|
||||
inner = append(inner, "--config", configPath)
|
||||
}
|
||||
return append(inner, "open", link)
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
// Command lazysecrets is a terminal UI for browsing secrets in Azure Key
|
||||
// Vault and Databricks secret scopes.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
tea "charm.land/bubbletea/v2"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/commands"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/config"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/instance"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/azure"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/databricks"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/repo"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ref"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/store"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/app"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/keys"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/styles"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "lazysecrets:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// looksLikeRef tells a reference given without "open" from a mistyped
|
||||
// subcommand, so the latter still gets the usage text.
|
||||
func looksLikeRef(s string) bool {
|
||||
return strings.Contains(s, "://") || strings.HasPrefix(s, "@Microsoft.KeyVault(") || strings.HasPrefix(s, "{{secrets/")
|
||||
}
|
||||
|
||||
// starredRepos are the repos the user starred something in: the repo, a
|
||||
// file or a key. Their records keep the repo's folder.
|
||||
func starredRepos(st *store.Store) []string {
|
||||
var out []string
|
||||
for key, n := range st.Nodes() {
|
||||
r, err := ref.Parse(key)
|
||||
if err == nil && r.Kind == provider.KindRepo && len(n.StarredBy) > 0 && n.Source != "" && !slices.Contains(out, n.Source) {
|
||||
out = append(out, n.Source)
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// checkCommandKeys reports a custom command key that a built-in action or
|
||||
// another command in an overlapping context takes.
|
||||
func checkCommandKeys(km *keys.Map, cmds []commands.Command) error {
|
||||
for i, c := range cmds {
|
||||
if c.Key == "" {
|
||||
continue
|
||||
}
|
||||
name := fmt.Sprintf("custom command %q", cmpOr(c.Description, c.Command))
|
||||
if err := km.CheckExtra(c.Key, name); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range cmds[:i] {
|
||||
overlap := c.Context == d.Context || c.Context == commands.Global || d.Context == commands.Global
|
||||
if d.Key == c.Key && overlap {
|
||||
return fmt.Errorf("%s: %q is also the key of custom command %q", name, c.Key, cmpOr(d.Description, d.Command))
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cmpOr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// defaultConfigPath is $LAZYSECRETS_CONFIG_FILE, else config.yaml in the
|
||||
// config directory.
|
||||
func defaultConfigPath() string {
|
||||
if env := os.Getenv("LAZYSECRETS_CONFIG_FILE"); env != "" {
|
||||
return env
|
||||
}
|
||||
dir, err := config.Dir()
|
||||
if err != nil {
|
||||
return "config.yaml"
|
||||
}
|
||||
return filepath.Join(dir, "config.yaml")
|
||||
}
|
||||
|
||||
func run() error {
|
||||
configPath := flag.String("config", defaultConfigPath(),
|
||||
"config file; several, comma-separated, are layered in order (also LAZYSECRETS_CONFIG_FILE)")
|
||||
showVersion := flag.Bool("version", false, "print the version and exit")
|
||||
dir := flag.String("dir", "", "start in this directory: its git repo's SOPS files, the ones under it first (default: the current directory)")
|
||||
demo := flag.Bool("demo", false, "browse built-in sample data instead of real backends")
|
||||
timeout := flag.Duration("timeout", 30*time.Second, "timeout for each backend request")
|
||||
icons := flag.Bool("icons", true,
|
||||
"show Nerd Font icons (needs a Nerd Font v3+); disable with --icons=false or LAZYSECRETS_ICONS=0")
|
||||
clipClear := flag.Duration("clipboard-clear", 30*time.Second, "clear copied values from the clipboard after this long (0 disables)")
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(flag.CommandLine.Output(), `Usage:
|
||||
lazysecrets [flags] browse secrets
|
||||
lazysecrets [flags] [open] <ref>
|
||||
open a lazysecrets:// link, Key Vault URL,
|
||||
@Microsoft.KeyVault(...) or {{secrets/scope/key}}
|
||||
lazysecrets [flags] get <ref> print the value a lazysecrets:// link points at,
|
||||
for scripts: VAR=$(lazysecrets get <ref>)
|
||||
lazysecrets [flags] keybindings print every action and its keys (with your
|
||||
config applied), as config to copy and edit
|
||||
lazysecrets handler install [--terminal NAME]
|
||||
open lazysecrets:// links from the browser and
|
||||
chat in a new window of this terminal
|
||||
lazysecrets handler uninstall stop opening them
|
||||
lazysecrets launch <ref> open a link in a new terminal window, as
|
||||
the link handler does
|
||||
lazysecrets update [--check] install the newest release in place of this
|
||||
one (--check: just say if there is one)
|
||||
|
||||
Flags:
|
||||
`)
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
flag.Parse()
|
||||
if *showVersion {
|
||||
fmt.Println("lazysecrets", versionString())
|
||||
return nil
|
||||
}
|
||||
|
||||
var open, getRef *ref.Ref
|
||||
printKeys := false
|
||||
switch args := flag.Args(); {
|
||||
case len(args) == 1 && args[0] == "keybindings":
|
||||
printKeys = true
|
||||
case len(args) == 2 && args[0] == "get":
|
||||
r, err := ref.Parse(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
getRef = &r
|
||||
case len(args) > 0 && args[0] == "handler":
|
||||
return handler(*configPath, args[1:])
|
||||
case len(args) > 0 && args[0] == "launch":
|
||||
return launch(*configPath, args[1:])
|
||||
case len(args) > 0 && args[0] == "update":
|
||||
return updateCmd(*configPath, args[1:])
|
||||
case len(args) == 0:
|
||||
case args[0] == "open" && len(args) == 2:
|
||||
r, err := ref.Parse(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
open = &r
|
||||
case len(args) == 1 && looksLikeRef(args[0]):
|
||||
// lazysecrets <link>, like lazysecrets open <link>
|
||||
r, err := ref.Parse(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
open = &r
|
||||
default:
|
||||
flag.Usage()
|
||||
return fmt.Errorf("unexpected arguments: %v", args)
|
||||
}
|
||||
|
||||
cfg, err := config.LoadAll(strings.Split(*configPath, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
theme, defaultFg, err := styles.FromTheme(cfg.Theme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
keymap := keys.Default()
|
||||
if err := keymap.Apply(cfg.Keybinding); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkCommandKeys(&keymap, cfg.CustomCommands); err != nil {
|
||||
return err
|
||||
}
|
||||
if printKeys {
|
||||
fmt.Print(keymap.YAML())
|
||||
return nil
|
||||
}
|
||||
// Precedence: flag > environment > config file > default.
|
||||
set := map[string]bool{}
|
||||
flag.Visit(func(f *flag.Flag) { set[f.Name] = true })
|
||||
if !set["timeout"] && cfg.Timeout > 0 {
|
||||
*timeout = cfg.Timeout
|
||||
}
|
||||
if !set["clipboard-clear"] && cfg.ClipboardClear != nil {
|
||||
*clipClear = *cfg.ClipboardClear
|
||||
}
|
||||
expiryDays := 30
|
||||
if cfg.ExpiryWarningDays != nil {
|
||||
expiryDays = *cfg.ExpiryWarningDays
|
||||
}
|
||||
revealTimeout := 60 * time.Second
|
||||
if cfg.RevealTimeout != nil {
|
||||
revealTimeout = *cfg.RevealTimeout
|
||||
}
|
||||
loadDelay := 150 * time.Millisecond
|
||||
if cfg.LoadDelay != nil {
|
||||
loadDelay = *cfg.LoadDelay
|
||||
}
|
||||
if !set["icons"] {
|
||||
switch {
|
||||
case os.Getenv("LAZYSECRETS_ICONS") != "":
|
||||
*icons = os.Getenv("LAZYSECRETS_ICONS") != "0"
|
||||
case cfg.Icons != nil:
|
||||
*icons = *cfg.Icons
|
||||
}
|
||||
}
|
||||
|
||||
// Stars, notes and the layout. Demo mode keeps them in memory, so
|
||||
// sample vaults never end up in the real file.
|
||||
stateDir, err := config.StateDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
statePath := ""
|
||||
if !*demo {
|
||||
statePath = filepath.Join(stateDir, "state.json")
|
||||
}
|
||||
st, err := store.Load(statePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var providers []provider.Provider
|
||||
if *demo {
|
||||
// Two Azure accounts, like a personal and an admin login in az.
|
||||
providers = []provider.Provider{
|
||||
fake.Azure(400*time.Millisecond).As("Azure:alice@example.com/demo", "Azure · alice"),
|
||||
fake.AzureOps(700*time.Millisecond).As("Azure:admin.alice@example.com/demo", "Azure · admin.alice"),
|
||||
fake.Databricks(600 * time.Millisecond),
|
||||
fake.Repos(),
|
||||
}
|
||||
} else {
|
||||
pc := cfg.Providers
|
||||
// One provider per az account. Created even when disabled: AKV-
|
||||
// backed Databricks scopes fall back to reading the vault through
|
||||
// the default account.
|
||||
accounts, err := azure.NewAccounts(azure.Settings{
|
||||
HideSubscriptions: pc.Azure.HideSubscriptions,
|
||||
Vaults: pc.Azure.Vaults,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("azure: %w", err)
|
||||
}
|
||||
if config.On(pc.Azure.Enabled) {
|
||||
for _, a := range accounts {
|
||||
providers = append(providers, a)
|
||||
}
|
||||
}
|
||||
if config.On(pc.Databricks.Enabled) {
|
||||
dbx, err := databricks.New(accounts[0], pc.Databricks.HideProfiles)
|
||||
if err != nil {
|
||||
return fmt.Errorf("databricks: %w", err)
|
||||
}
|
||||
providers = append(providers, dbx)
|
||||
}
|
||||
if config.On(pc.Repos.Enabled) {
|
||||
start := *dir
|
||||
if start == "" {
|
||||
start, _ = os.Getwd()
|
||||
}
|
||||
roots := []string{"~/git"}
|
||||
if pc.Repos.Roots != nil {
|
||||
roots = *pc.Repos.Roots
|
||||
}
|
||||
repos := repo.New(repo.Settings{
|
||||
Dir: start, Paths: pc.Repos.Paths, Roots: roots, Starred: starredRepos(st),
|
||||
Include: pc.Repos.Include, Exclude: pc.Repos.Exclude,
|
||||
})
|
||||
// The repo it started in (shown on top as Repo) and the
|
||||
// others, checked for secret files in the background like
|
||||
// the other sources; repos without any aren't listed.
|
||||
providers = append(providers, repos)
|
||||
}
|
||||
if len(providers) == 0 {
|
||||
return fmt.Errorf("all providers are disabled in %s", *configPath)
|
||||
}
|
||||
}
|
||||
|
||||
// Libraries (notably the Databricks SDK) log token refreshes through
|
||||
// the standard logger, which would draw over the TUI.
|
||||
log.SetOutput(io.Discard)
|
||||
|
||||
if getRef != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
defer cancel()
|
||||
return get(ctx, providers, st, *getRef)
|
||||
}
|
||||
|
||||
// Links opened elsewhere (lazysecrets launch) can come here.
|
||||
var p *tea.Program
|
||||
var srv *instance.Server
|
||||
if !*demo {
|
||||
srv, err = instance.Listen(instance.Dir(stateDir), func(link, front string) error {
|
||||
r, err := ref.Parse(link)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Accept at once: launch waits for the answer, and opens a
|
||||
// window anyway if it's slow. Coming to the front can be
|
||||
// (activating the terminal app).
|
||||
go func() {
|
||||
p.Send(app.OpenLink(r))
|
||||
instance.Raise(front)
|
||||
}()
|
||||
return nil
|
||||
})
|
||||
if err == nil {
|
||||
defer srv.Close()
|
||||
} // else links open in new windows: not worth failing for
|
||||
}
|
||||
var setup *app.HandlerSetup
|
||||
if !*demo {
|
||||
setup = handlerSetup(cfg, *configPath)
|
||||
}
|
||||
onActive := func() {}
|
||||
if srv != nil {
|
||||
onActive = srv.Touch
|
||||
}
|
||||
|
||||
// What was loaded last time, and where it was left in this folder.
|
||||
var cacheFile, placeDir string
|
||||
if !*demo {
|
||||
if d, err := config.CacheDir(); err == nil && config.On(cfg.Cache) {
|
||||
cacheFile = filepath.Join(d, "cache.json")
|
||||
}
|
||||
placeDir = *dir
|
||||
if placeDir == "" {
|
||||
placeDir, _ = os.Getwd()
|
||||
}
|
||||
if abs, err := filepath.Abs(placeDir); err == nil {
|
||||
placeDir = abs
|
||||
}
|
||||
}
|
||||
|
||||
m := app.New(providers, app.Options{
|
||||
Timeout: *timeout,
|
||||
Icons: *icons,
|
||||
ClipboardClear: *clipClear,
|
||||
FlatSources: cfg.SourcesView == "flat",
|
||||
CollapseGroups: config.On(cfg.CollapseGroups),
|
||||
LoadDelay: loadDelay,
|
||||
NoHistory: !config.On(cfg.History),
|
||||
OnActive: onActive,
|
||||
RevealTimeout: revealTimeout,
|
||||
ExpiryWarning: time.Duration(expiryDays) * 24 * time.Hour,
|
||||
HideOnBlur: config.On(cfg.HideOnBlur),
|
||||
HandlerSetup: setup,
|
||||
Updater: updater(cfg.Releases),
|
||||
UpdateCheck: config.On(cfg.UpdateCheck),
|
||||
Keys: &keymap,
|
||||
Commands: cfg.CustomCommands,
|
||||
Version: versionString(),
|
||||
Store: st,
|
||||
CacheFile: cacheFile,
|
||||
ConfigFiles: configFiles(*configPath),
|
||||
StatePath: statePath,
|
||||
Changelog: lazysecrets.Changelog,
|
||||
Dir: placeDir,
|
||||
Open: open,
|
||||
Styles: &theme,
|
||||
DefaultFg: defaultFg,
|
||||
})
|
||||
p = tea.NewProgram(m)
|
||||
final, err := p.Run()
|
||||
if fm, ok := final.(app.Model); ok {
|
||||
_ = fm.Persist() // a cache it couldn't save is no reason to fail
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// configFiles are the config files given, for the status pane: those
|
||||
// that exist, or the first if none does (where to create one).
|
||||
func configFiles(list string) []string {
|
||||
var all, found []string
|
||||
for _, f := range strings.Split(list, ",") {
|
||||
if f = strings.TrimSpace(f); f == "" {
|
||||
continue
|
||||
}
|
||||
all = append(all, f)
|
||||
if _, err := os.Stat(f); err == nil {
|
||||
found = append(found, f)
|
||||
}
|
||||
}
|
||||
if len(found) == 0 && len(all) > 0 {
|
||||
return all[:1]
|
||||
}
|
||||
return found
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/config"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/ui/app"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/update"
|
||||
)
|
||||
|
||||
// updater is the app's way to find and install releases, replacing the
|
||||
// binary that's running, from the servers (config releases) or the
|
||||
// default ones.
|
||||
func updater(servers []string) *app.Updater {
|
||||
c := &update.Client{Servers: servers}
|
||||
return &app.Updater{
|
||||
Latest: c.Latest,
|
||||
Install: func(ctx context.Context, r update.Release) error {
|
||||
exe, err := update.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.Install(ctx, r, exe)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// updateCmd is lazysecrets update [--check]: the newer release's
|
||||
// changelog, then (unless --check) install it.
|
||||
func updateCmd(configPath string, args []string) error {
|
||||
check := len(args) == 1 && args[0] == "--check"
|
||||
if len(args) > 0 && !check {
|
||||
return errors.New("usage: lazysecrets update [--check]")
|
||||
}
|
||||
cfg, err := config.LoadAll(strings.Split(configPath, ","))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current := versionString()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
c := &update.Client{Servers: cfg.Releases}
|
||||
r, err := c.Latest(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("looking for a newer lazysecrets: %w", err)
|
||||
}
|
||||
if !update.Newer(current, r.Tag) {
|
||||
if !update.Newer(current, "v999.0.0") {
|
||||
return fmt.Errorf("this is a development build (%s); the latest release is %s: install it with install.sh, or go install …@%s", current, r.Tag, r.Tag)
|
||||
}
|
||||
fmt.Printf("lazysecrets %s is the latest (%s)\n", current, r.Tag)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("lazysecrets %s is out (you have %s):\n\n%s\n\n", r.Tag, current, indent(r.Notes()))
|
||||
if check {
|
||||
fmt.Println("lazysecrets update installs it.")
|
||||
return nil
|
||||
}
|
||||
exe, err := update.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Updating %s…\n", exe)
|
||||
if err := c.Install(ctx, r, exe); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Updated to %s.\n", r.Tag)
|
||||
return nil
|
||||
}
|
||||
|
||||
func indent(s string) string {
|
||||
if s == "" {
|
||||
return " (no changelog)"
|
||||
}
|
||||
return " " + strings.ReplaceAll(s, "\n", "\n ")
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import "runtime/debug"
|
||||
|
||||
// version is set by release builds:
|
||||
//
|
||||
// go build -ldflags "-X main.version=1.2.3" ./cmd/lazysecrets
|
||||
var version string
|
||||
|
||||
// versionString is the release version, or for other builds what Go
|
||||
// recorded: the module version (go install …@v1.2.3), else the commit.
|
||||
func versionString() string {
|
||||
if version != "" {
|
||||
return version
|
||||
}
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return "dev"
|
||||
}
|
||||
if v := bi.Main.Version; v != "" && v != "(devel)" {
|
||||
return v
|
||||
}
|
||||
var rev, dirty string
|
||||
for _, s := range bi.Settings {
|
||||
switch s.Key {
|
||||
case "vcs.revision":
|
||||
rev = s.Value
|
||||
case "vcs.modified":
|
||||
if s.Value == "true" {
|
||||
dirty = "-dirty"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(rev) >= 7 {
|
||||
return "dev-" + rev[:7] + dirty
|
||||
}
|
||||
return "dev"
|
||||
}
|
||||
Reference in new issue
Block a user