feat: config grouped by area, with per-provider timeouts and an update interval #60
No files matched your search
+28
-5
@@ -3,6 +3,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -106,7 +107,7 @@ func run() error {
|
||||
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")
|
||||
timeout := flag.Duration("timeout", 30*time.Second, "timeout for each backend request, every provider's (overrides the config)")
|
||||
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)")
|
||||
@@ -207,9 +208,24 @@ Flags:
|
||||
// Precedence: flag > environment > config file > default.
|
||||
set := map[string]bool{}
|
||||
flag.Visit(func(f *flag.Flag) { set[f.Name] = true })
|
||||
if !set["timeout"] && cfg.Providers.Timeout > 0 {
|
||||
*timeout = cfg.Providers.Timeout
|
||||
// Each provider's own timeout, unless --timeout sets them all.
|
||||
timeouts := map[provider.Kind]time.Duration{}
|
||||
if !set["timeout"] {
|
||||
if cfg.Providers.Timeout > 0 {
|
||||
*timeout = cfg.Providers.Timeout
|
||||
}
|
||||
for k, d := range map[provider.Kind]time.Duration{
|
||||
provider.KindAzure: cfg.Providers.Azure.Timeout,
|
||||
provider.KindDatabricks: cfg.Providers.Databricks.Timeout,
|
||||
provider.KindRepo: cfg.Providers.Repos.Timeout,
|
||||
} {
|
||||
if d > 0 {
|
||||
timeouts[k] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
// timeoutFor is how long a request to a provider of kind k may take.
|
||||
timeoutFor := func(k provider.Kind) time.Duration { return cmp.Or(timeouts[k], *timeout) }
|
||||
if !set["clipboard-clear"] && cfg.Values.ClipboardClear != nil {
|
||||
*clipClear = *cfg.Values.ClipboardClear
|
||||
}
|
||||
@@ -314,12 +330,18 @@ Flags:
|
||||
log.SetOutput(io.Discard)
|
||||
|
||||
if getRef != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeoutFor(getRef.Kind))
|
||||
defer cancel()
|
||||
return get(ctx, providers, st, *getRef)
|
||||
}
|
||||
if shareArgs != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
||||
// The files from a repo, the key from whichever provider it's
|
||||
// in: the longest timeout.
|
||||
d := *timeout
|
||||
for _, t := range timeouts {
|
||||
d = max(d, t)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), d)
|
||||
defer cancel()
|
||||
return shareFiles(ctx, providers, st, cfg.Sharing.Key, shareRules(cfg), shareArgs, os.Stdout)
|
||||
}
|
||||
@@ -372,6 +394,7 @@ Flags:
|
||||
|
||||
m := app.New(providers, app.Options{
|
||||
Timeout: *timeout,
|
||||
Timeouts: timeouts,
|
||||
Icons: *icons,
|
||||
ClipboardClear: *clipClear,
|
||||
FlatSources: cfg.UI.SourcesView == "flat",
|
||||
|
||||
+4
-1
@@ -27,17 +27,20 @@ updates:
|
||||
- https://github.com/karleinarb/lazysecrets
|
||||
- https://git.kebcloud.no/keb/lazysecrets
|
||||
providers:
|
||||
timeout: 30s # per backend request
|
||||
timeout: 30s # per backend request; a provider's own timeout overrides it
|
||||
azure: # applies to every az account
|
||||
enabled: true # default
|
||||
timeout: 30s
|
||||
hideSubscriptions: [platform-mgmt] # by name or ID
|
||||
vaults: # listed under "Pinned vaults", for vaults
|
||||
- https://kv-foo.vault.azure.net/ # you can read but not discover
|
||||
databricks:
|
||||
enabled: true
|
||||
timeout: 30s
|
||||
hideProfiles: [test]
|
||||
repos:
|
||||
enabled: true
|
||||
timeout: 30s
|
||||
roots: [~/git]
|
||||
paths: [~/work/infra]
|
||||
include: ["secrets/**"]
|
||||
|
||||
@@ -123,7 +123,8 @@ type KeyRules struct {
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
// Timeout bounds each backend request; defaults to 30s.
|
||||
// Timeout bounds each backend request; defaults to 30s. Each
|
||||
// provider's own timeout overrides it for that provider.
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
Azure Azure `yaml:"azure"`
|
||||
Databricks Databricks `yaml:"databricks"`
|
||||
@@ -134,6 +135,8 @@ type Providers struct {
|
||||
// that repo is the only one shown; elsewhere, the ones in Paths and under
|
||||
// Roots.
|
||||
type Repos struct {
|
||||
// Timeout overrides providers.timeout for these requests.
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
// Enabled defaults to true.
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
Paths []string `yaml:"paths"`
|
||||
@@ -148,6 +151,8 @@ type Repos struct {
|
||||
}
|
||||
|
||||
type Azure struct {
|
||||
// Timeout overrides providers.timeout for these requests.
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
// Enabled defaults to true.
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
// HideSubscriptions lists subscriptions to leave out, by name or ID.
|
||||
@@ -158,6 +163,8 @@ type Azure struct {
|
||||
}
|
||||
|
||||
type Databricks struct {
|
||||
// Timeout overrides providers.timeout for these requests.
|
||||
Timeout time.Duration `yaml:"timeout"`
|
||||
// Enabled defaults to true.
|
||||
Enabled *bool `yaml:"enabled"`
|
||||
HideProfiles []string `yaml:"hideProfiles"`
|
||||
|
||||
@@ -29,6 +29,7 @@ storage:
|
||||
providers:
|
||||
timeout: 1m
|
||||
azure:
|
||||
timeout: 2m
|
||||
hideSubscriptions: [platform-mgmt]
|
||||
vaults:
|
||||
- https://kv-foo.vault.azure.net/
|
||||
@@ -40,7 +41,7 @@ providers:
|
||||
t.Fatal(err)
|
||||
}
|
||||
if c.UI.Icons == nil || *c.UI.Icons || c.Values.ClipboardClear == nil || *c.Values.ClipboardClear != 45*time.Second ||
|
||||
c.Providers.Timeout != time.Minute || len(c.Providers.Azure.Vaults) != 1 || c.Providers.Databricks.HideProfiles[0] != "test" {
|
||||
c.Providers.Timeout != time.Minute || c.Providers.Azure.Timeout != 2*time.Minute || len(c.Providers.Azure.Vaults) != 1 || c.Providers.Databricks.HideProfiles[0] != "test" {
|
||||
t.Errorf("config = %+v", c)
|
||||
}
|
||||
if c.UI.SourcesView != "flat" || On(c.Storage.History) || !On(c.Storage.Cache) {
|
||||
|
||||
+31
-14
@@ -65,6 +65,8 @@ const (
|
||||
type Options struct {
|
||||
// Timeout bounds every provider call. Defaults to 30s.
|
||||
Timeout time.Duration
|
||||
// Timeouts override Timeout for the providers of a kind.
|
||||
Timeouts map[provider.Kind]time.Duration
|
||||
// Icons shows Nerd Font glyphs next to backends.
|
||||
Icons bool
|
||||
// Clipboard defaults to the system clipboard, or OSC52 over SSH.
|
||||
@@ -146,7 +148,7 @@ type Clipboard interface {
|
||||
type Model struct {
|
||||
providers []provider.Provider
|
||||
byID map[string]provider.Provider
|
||||
timeout time.Duration
|
||||
timeout timeouts
|
||||
|
||||
keys keys.Map
|
||||
st styles.Styles
|
||||
@@ -378,7 +380,7 @@ func New(providers []provider.Provider, opts Options) Model {
|
||||
m := Model{
|
||||
providers: providers,
|
||||
byID: map[string]provider.Provider{},
|
||||
timeout: opts.Timeout,
|
||||
timeout: timeouts{def: opts.Timeout, byKind: opts.Timeouts},
|
||||
keys: keys.Default(),
|
||||
st: styles.Default(),
|
||||
spin: spinner.New(spinner.WithSpinner(spinner.Dot)),
|
||||
@@ -1246,7 +1248,7 @@ func (m *Model) showVersions(o provider.Object, key string) tea.Cmd {
|
||||
}
|
||||
m.secrets.SetLoading()
|
||||
p := m.providerFor(o.Container.Source)
|
||||
ctx, req := m.begin(slotObjects)
|
||||
ctx, req := m.begin(slotObjects, o.Container.Source.Kind)
|
||||
return func() tea.Msg {
|
||||
items, err := p.Versions(ctx, o)
|
||||
for i := range items {
|
||||
@@ -1276,7 +1278,7 @@ func (m *Model) countVersions(o provider.Object) tea.Cmd {
|
||||
}
|
||||
}
|
||||
p := m.providerFor(o.Container.Source)
|
||||
ctx, req := m.begin(slotVersions)
|
||||
ctx, req := m.begin(slotVersions, o.Container.Source.Kind)
|
||||
return m.settled(ctx, func() tea.Msg {
|
||||
items, err := p.Versions(ctx, o)
|
||||
for i := range items {
|
||||
@@ -1427,11 +1429,26 @@ func (m *Model) autoLoad(msg sourcesMsg) tea.Cmd {
|
||||
return m.crawlStart()
|
||||
}
|
||||
|
||||
// begin starts a cancellable request for pane p, cancelling any in flight.
|
||||
func (m *Model) begin(p slot) (context.Context, int) {
|
||||
// timeouts bound each provider request: its kind's, else the default.
|
||||
type timeouts struct {
|
||||
def time.Duration
|
||||
byKind map[provider.Kind]time.Duration
|
||||
}
|
||||
|
||||
// of is how long a request to a provider of kind k may take.
|
||||
func (t timeouts) of(k provider.Kind) time.Duration {
|
||||
if d, ok := t.byKind[k]; ok {
|
||||
return d
|
||||
}
|
||||
return t.def
|
||||
}
|
||||
|
||||
// begin starts a cancellable request for pane p to a provider of kind k,
|
||||
// cancelling any in flight.
|
||||
func (m *Model) begin(p slot, k provider.Kind) (context.Context, int) {
|
||||
m.cancelReq(p)
|
||||
m.seq++
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout.of(k))
|
||||
m.reqID[p], m.cancel[p] = m.seq, cancel
|
||||
return ctx, m.seq
|
||||
}
|
||||
@@ -1485,7 +1502,7 @@ func (m Model) sourcesCmd() tea.Cmd {
|
||||
gen := m.sourcesGen
|
||||
var cmds []tea.Cmd
|
||||
for _, p := range m.providers {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout.of(p.Kind()))
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
defer cancel()
|
||||
s, err := p.Sources(ctx)
|
||||
@@ -1496,7 +1513,7 @@ func (m Model) sourcesCmd() tea.Cmd {
|
||||
})
|
||||
// The source it started in, quickly, on its own.
|
||||
if c, ok := p.(provider.Current); ok {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), m.timeout.of(p.Kind()))
|
||||
cmds = append(cmds, func() tea.Msg {
|
||||
defer cancel()
|
||||
src, ok := c.Current(ctx)
|
||||
@@ -1513,7 +1530,7 @@ func (m Model) sourcesCmd() tea.Cmd {
|
||||
|
||||
func (m *Model) loadContainers(src provider.Source, key string) tea.Cmd {
|
||||
p := m.providerFor(src)
|
||||
ctx, req := m.begin(slotContainers)
|
||||
ctx, req := m.begin(slotContainers, src.Kind)
|
||||
return m.settled(ctx, func() tea.Msg {
|
||||
items, err := p.Containers(ctx, src)
|
||||
// Carry the source as given, with its provider ID, so later
|
||||
@@ -1532,7 +1549,7 @@ func (m *Model) loadContainers(src provider.Source, key string) tea.Cmd {
|
||||
func (m *Model) loadObjects(c provider.Container, t provider.ObjectType, key string) tea.Cmd {
|
||||
cands := m.candidates(c.Source)
|
||||
known := len(cands) == 1 && cands[0].ID() == c.Source.Provider
|
||||
ctx, req := m.begin(slotObjects)
|
||||
ctx, req := m.begin(slotObjects, c.Source.Kind)
|
||||
return m.settled(ctx, func() tea.Msg {
|
||||
var errs []error
|
||||
for _, p := range cands {
|
||||
@@ -1560,7 +1577,7 @@ func (m *Model) loadObjects(c provider.Container, t provider.ObjectType, key str
|
||||
|
||||
func (m *Model) fetchValue(s provider.Object) tea.Cmd {
|
||||
p := m.providerFor(s.Container.Source)
|
||||
ctx, req := m.begin(slotValue)
|
||||
ctx, req := m.begin(slotValue, s.Container.Source.Kind)
|
||||
m.detail.SetLoading()
|
||||
return func() tea.Msg {
|
||||
v, err := p.Value(ctx, s)
|
||||
@@ -1579,7 +1596,7 @@ func (m *Model) describe(o provider.Object) tea.Cmd {
|
||||
m.detail.SetDescribing()
|
||||
}
|
||||
p := m.providerFor(o.Container.Source)
|
||||
ctx, req := m.begin(slotDescribe)
|
||||
ctx, req := m.begin(slotDescribe, o.Container.Source.Kind)
|
||||
return m.settled(ctx, func() tea.Msg {
|
||||
obj, err := p.Describe(ctx, o)
|
||||
return describeMsg{req: req, key: key, obj: obj, err: err}
|
||||
@@ -1607,7 +1624,7 @@ func (m *Model) copyValue() tea.Cmd {
|
||||
return m.copyText(o.Name, v.Reveal(), true)
|
||||
}
|
||||
p := m.providerFor(o.Container.Source)
|
||||
ctx, req := m.begin(slotCopy)
|
||||
ctx, req := m.begin(slotCopy, o.Container.Source.Kind)
|
||||
m.flash = flash{text: "copying " + o.Name + "…"}
|
||||
return func() tea.Msg {
|
||||
v, err := p.Value(ctx, o)
|
||||
|
||||
@@ -165,7 +165,7 @@ func (m *Model) runCommand(c commands.Command, rendered string, obj *provider.Ob
|
||||
o := *obj
|
||||
p := m.providerFor(o.Container.Source)
|
||||
m.flash = flash{text: "fetching " + o.Name + " for " + c.Description + "…"}
|
||||
timeout := m.timeout
|
||||
timeout := m.timeout.of(o.Container.Source.Kind)
|
||||
m.usedObject(o)
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
|
||||
@@ -196,7 +196,7 @@ func (m *Model) loadDetails(c provider.Container) tea.Cmd {
|
||||
}
|
||||
}
|
||||
m.details[key] = detailsState{loading: true}
|
||||
ctx, req := m.begin(slotDetails)
|
||||
ctx, req := m.begin(slotDetails, c.Source.Kind)
|
||||
return m.settled(ctx, func() tea.Msg {
|
||||
attrs, err := d.Details(ctx, c)
|
||||
if ctx.Err() != nil {
|
||||
|
||||
@@ -158,7 +158,7 @@ func (m *Model) copyFromRef(r ref.Ref) tea.Cmd {
|
||||
m.flash = flash{text: "copy: no account or profile for " + describeRef(r), isErr: true}
|
||||
return nil
|
||||
}
|
||||
ctx, req := m.begin(slotCopy)
|
||||
ctx, req := m.begin(slotCopy, r.Kind)
|
||||
m.flash = flash{text: "copying " + r.Name + "…"}
|
||||
return func() tea.Msg {
|
||||
var errs []error
|
||||
|
||||
@@ -295,10 +295,10 @@ func (m *Model) crawlNext() tea.Cmd {
|
||||
}
|
||||
|
||||
func (m *Model) crawlCmd(job crawlJob) tea.Cmd {
|
||||
timeout := m.timeout
|
||||
if job.src != nil {
|
||||
p := m.providerFor(*job.src)
|
||||
src := *job.src
|
||||
timeout := m.timeout.of(src.Kind)
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := contextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
@@ -311,6 +311,7 @@ func (m *Model) crawlCmd(job crawlJob) tea.Cmd {
|
||||
}
|
||||
c := *job.container
|
||||
p := m.providerFor(c.Source)
|
||||
timeout := m.timeout.of(c.Source.Kind)
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := contextWithTimeout(timeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -59,7 +59,7 @@ func (m *Model) shareFiles(src provider.Source, cs []provider.Container) tea.Cmd
|
||||
m.flash = warning("these files can't be shared")
|
||||
return nil
|
||||
}
|
||||
general, timeout := m.shareKey, m.timeout
|
||||
general, timeout := m.shareKey, m.timeout.of(src.Kind)
|
||||
m.flash = flash{text: "reading the files…"}
|
||||
return func() tea.Msg {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
@@ -216,7 +216,7 @@ func (m *Model) handleSealed(msg sealedMsg) tea.Cmd {
|
||||
func (m *Model) keyReader() func(ref.Ref) (provider.SecretValue, string, error) {
|
||||
providers, st, timeout := m.providers, m.store, m.timeout
|
||||
return func(key ref.Ref) (provider.SecretValue, string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout.of(key.Kind))
|
||||
defer cancel()
|
||||
k := key
|
||||
k.Version = ""
|
||||
@@ -258,7 +258,7 @@ func (m *Model) offerShare(r ref.Ref) tea.Cmd {
|
||||
// openShare reads the key, decrypts the files and finds the repo's clone.
|
||||
func (m *Model) openShare(s share.Sealed) tea.Cmd {
|
||||
read := m.keyReader()
|
||||
providers, timeout := m.providers, m.timeout
|
||||
providers, timeout := m.providers, m.timeout.of(provider.KindRepo)
|
||||
m.flash = flash{text: "decrypting with " + s.Key.Name + "…"}
|
||||
return func() tea.Msg {
|
||||
msg := shareOpenedMsg{sealed: s}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||
"git.kebcloud.no/keb/lazysecrets/internal/provider/fake"
|
||||
)
|
||||
|
||||
// A provider's own timeout applies to it alone.
|
||||
func TestTimeoutPerProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
d := newDriverOpts(t, Options{Timeout: 5 * time.Second, Timeouts: map[provider.Kind]time.Duration{provider.KindAzure: 10 * time.Millisecond}},
|
||||
fake.Azure(300*time.Millisecond), fake.Databricks(300*time.Millisecond))
|
||||
scr := d.screen()
|
||||
if !strings.Contains(scr, "✗ Azure: context deadline exceeded") || !strings.Contains(scr, "Databricks (…)") ||
|
||||
strings.Contains(scr, "Databricks: context deadline") {
|
||||
t.Errorf("want Azure timed out and Databricks still loading:\n%s", scr)
|
||||
}
|
||||
if got := (timeouts{def: time.Minute}).of(provider.KindRepo); got != time.Minute {
|
||||
t.Errorf("no override: %s, want the default", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user