Hello world 👋
Release / Build and publish (push) Successful in 23s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kebandClaude Opus 5.5 committed 2026-10-10 22:34:22 +02:00
commit 430875f200
111 files changed
+22958

No files matched your search

+114
View File
@@ -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...)
}
+50
View File
@@ -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)
}
}
+195
View File
@@ -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)
}
+363
View File
@@ -0,0 +1,363 @@
// 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/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(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:dev@example.com/demo", "Azure · dev"),
fake.AzureOps(700*time.Millisecond).As("Azure:ops@example.com/demo", "Azure · ops"),
fake.Databricks(600 * time.Millisecond),
}
} 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
}
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(),
UpdateCheck: config.On(cfg.UpdateCheck),
Keys: &keymap,
Commands: cfg.CustomCommands,
Version: versionString(),
Store: st,
Open: open,
Styles: &theme,
DefaultFg: defaultFg,
})
p = tea.NewProgram(m)
_, err = p.Run()
return err
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"context"
"errors"
"fmt"
"strings"
"time"
"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.
func updater() *app.Updater {
c := &update.Client{}
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(args []string) error {
check := len(args) == 1 && args[0] == "--check"
if len(args) > 0 && !check {
return errors.New("usage: lazysecrets update [--check]")
}
current := versionString()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
c := &update.Client{}
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 ")
}
+38
View File
@@ -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"
}