Compare commits
2
Commits
4e4014a043
...
ab8d8077b0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab8d8077b0 | ||
|
|
10d1fc63c7 |
No files matched your search
@@ -99,10 +99,10 @@ use most:
|
|||||||
|
|
||||||
lazysecrets never stores, caches or sends your secret values. It keeps the
|
lazysecrets never stores, caches or sends your secret values. It keeps the
|
||||||
names and metadata of what you've loaded in a cache file readable only by
|
names and metadata of what you've loaded in a cache file readable only by
|
||||||
you, to show it at once next time (`cache: false` turns it off). It talks
|
you, to show it at once next time (`storage.cache: false` turns it off). It talks
|
||||||
only to the services you use it with, and collects no usage data. The one
|
only to the services you use it with, and collects no usage data. The one
|
||||||
call of its own is a daily check for a new release, on GitHub or else
|
call of its own is a daily check for a new release, on GitHub or else
|
||||||
git.kebcloud.no, which `updateCheck: false` turns off.
|
git.kebcloud.no, which `updates.autoCheck: false` turns off.
|
||||||
|
|
||||||
## Repositories
|
## Repositories
|
||||||
|
|
||||||
|
|||||||
+52
-24
@@ -3,6 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -106,7 +107,7 @@ func run() error {
|
|||||||
showVersion := flag.Bool("version", false, "print the version and exit")
|
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)")
|
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")
|
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,
|
icons := flag.Bool("icons", true,
|
||||||
"show Nerd Font icons (needs a Nerd Font v3+); disable with --icons=false or LAZYSECRETS_ICONS=0")
|
"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)")
|
clipClear := flag.Duration("clipboard-clear", 30*time.Second, "clear copied values from the clipboard after this long (0 disables)")
|
||||||
@@ -207,30 +208,49 @@ Flags:
|
|||||||
// Precedence: flag > environment > config file > default.
|
// Precedence: flag > environment > config file > default.
|
||||||
set := map[string]bool{}
|
set := map[string]bool{}
|
||||||
flag.Visit(func(f *flag.Flag) { set[f.Name] = true })
|
flag.Visit(func(f *flag.Flag) { set[f.Name] = true })
|
||||||
if !set["timeout"] && cfg.Timeout > 0 {
|
// Each provider's own timeout, unless --timeout sets them all.
|
||||||
*timeout = cfg.Timeout
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !set["clipboard-clear"] && cfg.ClipboardClear != nil {
|
// timeoutFor is how long a request to a provider of kind k may take.
|
||||||
*clipClear = *cfg.ClipboardClear
|
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
|
||||||
}
|
}
|
||||||
expiryDays := 30
|
expiryDays := 30
|
||||||
if cfg.ExpiryWarningDays != nil {
|
if cfg.UI.ExpiryWarningDays != nil {
|
||||||
expiryDays = *cfg.ExpiryWarningDays
|
expiryDays = *cfg.UI.ExpiryWarningDays
|
||||||
}
|
}
|
||||||
revealTimeout := 60 * time.Second
|
revealTimeout := 60 * time.Second
|
||||||
if cfg.RevealTimeout != nil {
|
if cfg.Values.RevealTimeout != nil {
|
||||||
revealTimeout = *cfg.RevealTimeout
|
revealTimeout = *cfg.Values.RevealTimeout
|
||||||
}
|
}
|
||||||
loadDelay := 150 * time.Millisecond
|
loadDelay := 150 * time.Millisecond
|
||||||
if cfg.LoadDelay != nil {
|
if cfg.UI.LoadDelay != nil {
|
||||||
loadDelay = *cfg.LoadDelay
|
loadDelay = *cfg.UI.LoadDelay
|
||||||
|
}
|
||||||
|
var updateEvery time.Duration // 0: the app's default, a day
|
||||||
|
if cfg.Updates.AutoCheckInterval != nil {
|
||||||
|
updateEvery = *cfg.Updates.AutoCheckInterval
|
||||||
}
|
}
|
||||||
if !set["icons"] {
|
if !set["icons"] {
|
||||||
switch {
|
switch {
|
||||||
case os.Getenv("LAZYSECRETS_ICONS") != "":
|
case os.Getenv("LAZYSECRETS_ICONS") != "":
|
||||||
*icons = os.Getenv("LAZYSECRETS_ICONS") != "0"
|
*icons = os.Getenv("LAZYSECRETS_ICONS") != "0"
|
||||||
case cfg.Icons != nil:
|
case cfg.UI.Icons != nil:
|
||||||
*icons = *cfg.Icons
|
*icons = *cfg.UI.Icons
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,14 +330,20 @@ Flags:
|
|||||||
log.SetOutput(io.Discard)
|
log.SetOutput(io.Discard)
|
||||||
|
|
||||||
if getRef != nil {
|
if getRef != nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
|
ctx, cancel := context.WithTimeout(context.Background(), timeoutFor(getRef.Kind))
|
||||||
defer cancel()
|
defer cancel()
|
||||||
return get(ctx, providers, st, *getRef)
|
return get(ctx, providers, st, *getRef)
|
||||||
}
|
}
|
||||||
if shareArgs != nil {
|
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()
|
defer cancel()
|
||||||
return shareFiles(ctx, providers, st, cfg.Share.Key, shareRules(cfg), shareArgs, os.Stdout)
|
return shareFiles(ctx, providers, st, cfg.Sharing.Key, shareRules(cfg), shareArgs, os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Links opened elsewhere (lazysecrets launch) can come here.
|
// Links opened elsewhere (lazysecrets launch) can come here.
|
||||||
@@ -354,7 +380,7 @@ Flags:
|
|||||||
// What was loaded last time, and where it was left in this folder.
|
// What was loaded last time, and where it was left in this folder.
|
||||||
var cacheFile, placeDir string
|
var cacheFile, placeDir string
|
||||||
if !*demo {
|
if !*demo {
|
||||||
if d, err := config.CacheDir(); err == nil && config.On(cfg.Cache) {
|
if d, err := config.CacheDir(); err == nil && config.On(cfg.Storage.Cache) {
|
||||||
cacheFile = filepath.Join(d, "cache.json")
|
cacheFile = filepath.Join(d, "cache.json")
|
||||||
}
|
}
|
||||||
placeDir = *dir
|
placeDir = *dir
|
||||||
@@ -368,19 +394,21 @@ Flags:
|
|||||||
|
|
||||||
m := app.New(providers, app.Options{
|
m := app.New(providers, app.Options{
|
||||||
Timeout: *timeout,
|
Timeout: *timeout,
|
||||||
|
Timeouts: timeouts,
|
||||||
Icons: *icons,
|
Icons: *icons,
|
||||||
ClipboardClear: *clipClear,
|
ClipboardClear: *clipClear,
|
||||||
FlatSources: cfg.SourcesView == "flat",
|
FlatSources: cfg.UI.SourcesView == "flat",
|
||||||
CollapseGroups: config.On(cfg.CollapseGroups),
|
CollapseGroups: config.On(cfg.UI.CollapseGroups),
|
||||||
LoadDelay: loadDelay,
|
LoadDelay: loadDelay,
|
||||||
NoHistory: !config.On(cfg.History),
|
NoHistory: !config.On(cfg.Storage.History),
|
||||||
OnActive: onActive,
|
OnActive: onActive,
|
||||||
RevealTimeout: revealTimeout,
|
RevealTimeout: revealTimeout,
|
||||||
ExpiryWarning: time.Duration(expiryDays) * 24 * time.Hour,
|
ExpiryWarning: time.Duration(expiryDays) * 24 * time.Hour,
|
||||||
HideOnBlur: config.On(cfg.HideOnBlur),
|
HideOnBlur: config.On(cfg.Values.HideOnBlur),
|
||||||
HandlerSetup: setup,
|
HandlerSetup: setup,
|
||||||
Updater: updater(cfg.Releases),
|
Updater: updater(cfg.Updates.UpdateSources),
|
||||||
UpdateCheck: config.On(cfg.UpdateCheck),
|
UpdateCheck: config.On(cfg.Updates.AutoCheck),
|
||||||
|
UpdateEvery: updateEvery,
|
||||||
Keys: &keymap,
|
Keys: &keymap,
|
||||||
Commands: cfg.CustomCommands,
|
Commands: cfg.CustomCommands,
|
||||||
Version: versionString(),
|
Version: versionString(),
|
||||||
@@ -390,7 +418,7 @@ Flags:
|
|||||||
StatePath: statePath,
|
StatePath: statePath,
|
||||||
Changelog: lazysecrets.Changelog,
|
Changelog: lazysecrets.Changelog,
|
||||||
Dir: placeDir,
|
Dir: placeDir,
|
||||||
ShareKey: cfg.Share.Key,
|
ShareKey: cfg.Sharing.Key,
|
||||||
ShareRules: shareRules(cfg),
|
ShareRules: shareRules(cfg),
|
||||||
Open: open,
|
Open: open,
|
||||||
Styles: &theme,
|
Styles: &theme,
|
||||||
|
|||||||
@@ -89,5 +89,5 @@ func shareFiles(ctx context.Context, providers []provider.Provider, st *store.St
|
|||||||
|
|
||||||
// shareRules are the config's share.keyRules.
|
// shareRules are the config's share.keyRules.
|
||||||
func shareRules(cfg config.Config) share.Rules {
|
func shareRules(cfg config.Config) share.Rules {
|
||||||
return share.Rules{MinLength: cfg.Share.KeyRules.MinLength, MinClasses: cfg.Share.KeyRules.MinClasses}
|
return share.Rules{MinLength: cfg.Sharing.KeyRules.MinLength, MinClasses: cfg.Sharing.KeyRules.MinClasses}
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func updateCmd(configPath string, args []string) error {
|
|||||||
current := versionString()
|
current := versionString()
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
c := &update.Client{Servers: cfg.Releases}
|
c := &update.Client{Servers: cfg.Updates.UpdateSources}
|
||||||
r, err := c.Latest(ctx)
|
r, err := c.Latest(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("looking for a newer lazysecrets: %w", err)
|
return fmt.Errorf("looking for a newer lazysecrets: %w", err)
|
||||||
|
|||||||
+1
-1
@@ -63,7 +63,7 @@ Secrets, keys and certificates, each on their own tab of the Secrets pane
|
|||||||
(`[`/`]`). `space` reveals a secret's value, or the public material of a
|
(`[`/`]`). `space` reveals a secret's value, or the public material of a
|
||||||
key (PEM public key) or certificate; private key material is never
|
key (PEM public key) or certificate; private key material is never
|
||||||
fetched. `v` lists an item's versions. Items expiring within
|
fetched. `v` lists an item's versions. Items expiring within
|
||||||
`expiryWarningDays` (30 by default) are marked `⚠ 12d`, expired ones `✗`.
|
`ui.expiryWarningDays` (30 by default) are marked `⚠ 12d`, expired ones `✗`.
|
||||||
|
|
||||||
The status pane (`0`) lists them across every vault loaded, opened or
|
The status pane (`0`) lists them across every vault loaded, opened or
|
||||||
cached, soonest first, and its line counts them (`⚠ 3 expiring`); `enter`
|
cached, soonest first, and its line counts them (`⚠ 3 expiring`); `enter`
|
||||||
|
|||||||
+34
-25
@@ -7,37 +7,45 @@ status pane (`0`) opens it in your editor, as in lazygit (making it if
|
|||||||
it isn't there yet); changes apply when lazysecrets starts again.
|
it isn't there yet); changes apply when lazysecrets starts again.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
icons: true # Nerd Font icons (v3+); also --icons=false or LAZYSECRETS_ICONS=0
|
ui:
|
||||||
clipboardClear: 30s # clear copied values from the clipboard after this long; 0s disables
|
icons: true # Nerd Font icons (v3+); also --icons=false or LAZYSECRETS_ICONS=0
|
||||||
revealTimeout: 60s # hide a revealed value after this long; 0s keeps it shown
|
sourcesView: grouped # or flat; ` toggles it
|
||||||
hideOnBlur: true # hide a revealed value when the terminal loses focus (tmux: focus-events on)
|
collapseGroups: true # start with the Sources pane's groups folded; enter or space unfolds one
|
||||||
expiryWarningDays: 30 # mark secrets, keys and certificates expiring this soon (⚠ 12d); expired ones get ✗
|
loadDelay: 150ms # how long the cursor rests on a row before it loads; 0s loads at once
|
||||||
timeout: 30s # per backend request
|
expiryWarningDays: 30 # mark secrets, keys and certificates expiring this soon (⚠ 12d); expired ones get ✗
|
||||||
loadDelay: 150ms # how long the cursor rests on a row before it loads; 0s loads at once
|
values:
|
||||||
sourcesView: grouped # or flat; ` toggles it
|
revealTimeout: 60s # hide a revealed value after this long; 0s keeps it shown
|
||||||
collapseGroups: true # start with the Sources pane's groups folded; enter or space unfolds one
|
hideOnBlur: true # hide a revealed value when the terminal loses focus (tmux: focus-events on)
|
||||||
updateCheck: true # look for a newer release once a day (U and lazysecrets update work either way)
|
clipboardClear: 30s # clear copied values from the clipboard after this long; 0s disables
|
||||||
releases: # where updates come from, tried in order (GitHub, or a Gitea or Forgejo server)
|
storage:
|
||||||
- https://github.com/karleinarb/lazysecrets
|
cache: true # keep what was loaded (never values) for the next start
|
||||||
- https://git.kebcloud.no/keb/lazysecrets
|
history: true # count uses of items for Recent (R)
|
||||||
history: true # count uses of items for Recent (R)
|
updates:
|
||||||
cache: true # keep what was loaded (never values) for the next start
|
autoCheck: true # look for a newer release at startup (U and lazysecrets update work either way)
|
||||||
|
autoCheckInterval: 24h # at most this often
|
||||||
|
updateSources: # where updates come from, tried in order (GitHub, or a Gitea or Forgejo server)
|
||||||
|
- https://github.com/karleinarb/lazysecrets
|
||||||
|
- https://git.kebcloud.no/keb/lazysecrets
|
||||||
providers:
|
providers:
|
||||||
|
timeout: 30s # per backend request; a provider's own timeout overrides it
|
||||||
azure: # applies to every az account
|
azure: # applies to every az account
|
||||||
enabled: true # default
|
enabled: true # default
|
||||||
|
timeout: 30s
|
||||||
hideSubscriptions: [platform-mgmt] # by name or ID
|
hideSubscriptions: [platform-mgmt] # by name or ID
|
||||||
vaults: # listed under "Pinned vaults", for vaults
|
vaults: # listed under "Pinned vaults", for vaults
|
||||||
- https://kv-foo.vault.azure.net/ # you can read but not discover
|
- https://kv-foo.vault.azure.net/ # you can read but not discover
|
||||||
databricks:
|
databricks:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
timeout: 30s
|
||||||
hideProfiles: [test]
|
hideProfiles: [test]
|
||||||
repos:
|
repos:
|
||||||
enabled: true
|
enabled: true
|
||||||
|
timeout: 30s
|
||||||
roots: [~/git]
|
roots: [~/git]
|
||||||
paths: [~/work/infra]
|
paths: [~/work/infra]
|
||||||
include: ["secrets/**"]
|
include: ["secrets/**"]
|
||||||
exclude: ["**/testdata/**"]
|
exclude: ["**/testdata/**"]
|
||||||
share: # sharing env files as encrypted links
|
sharing: # sharing env files as encrypted links
|
||||||
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link; for repos naming none
|
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link; for repos naming none
|
||||||
keyRules: # what a key's value must be
|
keyRules: # what a key's value must be
|
||||||
minLength: 32 # characters, 16 at least
|
minLength: 32 # characters, 16 at least
|
||||||
@@ -45,7 +53,7 @@ share: # sharing env files as encrypted links
|
|||||||
```
|
```
|
||||||
|
|
||||||
More, each on its own page: [repos](repos.md) (`providers.repos`),
|
More, each on its own page: [repos](repos.md) (`providers.repos`),
|
||||||
[sharing env files](links.md#sharing-env-files) (`share`), the
|
[sharing env files](links.md#sharing-env-files) (`sharing`), the
|
||||||
[link handler](links.md#from-the-browser-and-chat) (`linkHandler`),
|
[link handler](links.md#from-the-browser-and-chat) (`linkHandler`),
|
||||||
[keybindings](keys.md#rebinding-keys) (`keybinding`) and
|
[keybindings](keys.md#rebinding-keys) (`keybinding`) and
|
||||||
[custom commands](custom-commands.md) (`customCommands`).
|
[custom commands](custom-commands.md) (`customCommands`).
|
||||||
@@ -84,7 +92,7 @@ theme:
|
|||||||
```
|
```
|
||||||
|
|
||||||
Icons need a [Nerd Font](https://www.nerdfonts.com) (v3+). If they render
|
Icons need a [Nerd Font](https://www.nerdfonts.com) (v3+). If they render
|
||||||
as boxes, turn them off with `icons: false`.
|
as boxes, turn them off with `ui.icons: false`.
|
||||||
|
|
||||||
## Layering
|
## Layering
|
||||||
|
|
||||||
@@ -112,7 +120,7 @@ lazysecrets windows can save to it without losing each other's changes.
|
|||||||
Revealing, copying, opening in the browser, sharing and jumping to an item
|
Revealing, copying, opening in the browser, sharing and jumping to an item
|
||||||
count as using it: its record keeps when it was last used and how often,
|
count as using it: its record keeps when it was last used and how often,
|
||||||
for Recent (`R`). Records kept only for usage are dropped after 90 days
|
for Recent (`R`). Records kept only for usage are dropped after 90 days
|
||||||
unused, and beyond the 100 most recent. `history: false` stops counting.
|
unused, and beyond the 100 most recent. `storage.history: false` stops counting.
|
||||||
|
|
||||||
It also keeps where you left lazysecrets in each folder (the selected
|
It also keeps where you left lazysecrets in each folder (the selected
|
||||||
source, vault or secret), and goes back there the next time it starts in
|
source, vault or secret), and goes back there the next time it starts in
|
||||||
@@ -132,15 +140,16 @@ and copying always fetch the value live.
|
|||||||
Lists not loaded for 30 days are dropped, and so is everything from an
|
Lists not loaded for 30 days are dropped, and so is everything from an
|
||||||
account you're no longer signed in to. Repos aren't cached: their files
|
account you're no longer signed in to. Repos aren't cached: their files
|
||||||
are read from disk anyway. Deleting the file is always safe, and
|
are read from disk anyway. Deleting the file is always safe, and
|
||||||
`cache: false` stops using it.
|
`storage.cache: false` stops using it.
|
||||||
|
|
||||||
## Updates
|
## Updates
|
||||||
|
|
||||||
Once a day (`updateCheck`), in the background, lazysecrets asks for the
|
At startup, in the background, lazysecrets asks for the latest release
|
||||||
latest release; within the day it reuses the answer from the state file.
|
(`updates.autoCheck`), once a day or every `updates.autoCheckInterval`;
|
||||||
Releases are published on GitHub and on git.kebcloud.no, and it asks
|
until then it reuses the answer from the state file. Releases are
|
||||||
them in that order, the next when one doesn't answer within 5 seconds;
|
published on GitHub and on git.kebcloud.no, and it asks them in that
|
||||||
`releases` lists others, such as a mirror of your own. Offline, or with
|
order, the next when one doesn't answer within 5 seconds;
|
||||||
|
`updates.updateSources` lists others, such as a mirror of your own. Offline, or with
|
||||||
every server down, it says nothing. A newer release shows in the footer, `v0.1.1 ↑ v0.2.0`; `U` (or
|
every server down, it says nothing. A newer release shows in the footer, `v0.1.1 ↑ v0.2.0`; `U` (or
|
||||||
`lazysecrets update --check`) shows what changed since your version, across the last 10 releases.
|
`lazysecrets update --check`) shows what changed since your version, across the last 10 releases.
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -65,8 +65,8 @@ certificates as their subject, issuer, dates, names and thumbprint, with
|
|||||||
any other PEM block (a private key) named but not shown. `y` still copies
|
any other PEM block (a private key) named but not shown. `y` still copies
|
||||||
it exactly as stored.
|
it exactly as stored.
|
||||||
|
|
||||||
A revealed value hides again after `revealTimeout` (60s) and when the
|
A revealed value hides again after `values.revealTimeout` (60s) and
|
||||||
terminal loses focus (`hideOnBlur`); see [config](config.md).
|
when the terminal loses focus (`values.hideOnBlur`); see [config](config.md).
|
||||||
|
|
||||||
## Stars and notes
|
## Stars and notes
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -54,7 +54,7 @@ uses it) or your config (for repos that don't name one), by its
|
|||||||
reference:
|
reference:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
share:
|
sharing:
|
||||||
key: https://kv-team.vault.azure.net/secrets/env-share
|
key: https://kv-team.vault.azure.net/secrets/env-share
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ isn't long and varied enough, saying how (never showing it). By default
|
|||||||
that's 32 characters of two kinds; your config can ask for more, or less:
|
that's 32 characters of two kinds; your config can ask for more, or less:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
share:
|
sharing:
|
||||||
keyRules:
|
keyRules:
|
||||||
minLength: 32 # characters; 16 at least
|
minLength: 32 # characters; 16 at least
|
||||||
minClasses: 2 # kinds of character: lowercase, uppercase, digits, symbols
|
minClasses: 2 # kinds of character: lowercase, uppercase, digits, symbols
|
||||||
|
|||||||
+1
-1
@@ -60,7 +60,7 @@ A repo's `.lazysecrets.yaml` can also name the secret its env files are
|
|||||||
[shared](links.md#sharing-env-files) with:
|
[shared](links.md#sharing-env-files) with:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
share:
|
sharing:
|
||||||
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link
|
key: https://kv-team.vault.azure.net/secrets/env-share # or its lazysecrets:// link
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+86
-54
@@ -19,44 +19,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Icons *bool `yaml:"icons"`
|
UI UI `yaml:"ui"`
|
||||||
ClipboardClear *time.Duration `yaml:"clipboardClear"`
|
// Values keeps revealed and copied values from lingering.
|
||||||
// RevealTimeout hides a revealed value after this long; 0s never.
|
Values Values `yaml:"values"`
|
||||||
// Defaults to 60s.
|
// Storage is what's kept between runs.
|
||||||
RevealTimeout *time.Duration `yaml:"revealTimeout"`
|
Storage Storage `yaml:"storage"`
|
||||||
// ExpiryWarningDays marks secrets, keys and certificates expiring
|
// Updates sets how lazysecrets looks for newer releases.
|
||||||
// within this many days; 0 marks only expired ones. Defaults to 30.
|
Updates Updates `yaml:"updates"`
|
||||||
ExpiryWarningDays *int `yaml:"expiryWarningDays"`
|
Providers Providers `yaml:"providers"`
|
||||||
// HideOnBlur hides a revealed value when the terminal loses focus.
|
|
||||||
// Defaults to true.
|
|
||||||
HideOnBlur *bool `yaml:"hideOnBlur"`
|
|
||||||
Timeout time.Duration `yaml:"timeout"`
|
|
||||||
// LoadDelay is how long the cursor must rest on a row before its
|
|
||||||
// contents load; 0s loads at once. Defaults to 150ms.
|
|
||||||
LoadDelay *time.Duration `yaml:"loadDelay"`
|
|
||||||
// SourcesView is "grouped" (default) or "flat"; ` + "`" + ` toggles it.
|
|
||||||
SourcesView string `yaml:"sourcesView"`
|
|
||||||
// History counts uses of items for the Recent list; defaults to true.
|
|
||||||
History *bool `yaml:"history"`
|
|
||||||
// UpdateCheck looks for a newer release once a day, in the
|
|
||||||
// background; defaults to true. U and lazysecrets update work either
|
|
||||||
// way.
|
|
||||||
UpdateCheck *bool `yaml:"updateCheck"`
|
|
||||||
// Releases are the repositories updates come from, tried in order:
|
|
||||||
// on GitHub, or a Gitea or Forgejo server. Defaults to GitHub, then
|
|
||||||
// git.kebcloud.no (update.DefaultServers).
|
|
||||||
Releases []string `yaml:"releases"`
|
|
||||||
// Cache keeps what was loaded (never values) between runs, so lists
|
|
||||||
// show at once and refresh in the background; defaults to true.
|
|
||||||
Cache *bool `yaml:"cache"`
|
|
||||||
// CollapseGroups starts the Sources pane with its groups folded;
|
|
||||||
// defaults to true.
|
|
||||||
CollapseGroups *bool `yaml:"collapseGroups"`
|
|
||||||
Providers Providers `yaml:"providers"`
|
|
||||||
// LinkHandler sets how lazysecrets:// links open; see linkhandler.
|
// LinkHandler sets how lazysecrets:// links open; see linkhandler.
|
||||||
LinkHandler LinkHandler `yaml:"linkHandler"`
|
LinkHandler LinkHandler `yaml:"linkHandler"`
|
||||||
// Share sets how env files are shared; see package share.
|
// Sharing sets how env files are shared; see package share.
|
||||||
Share Share `yaml:"share"`
|
Sharing Sharing `yaml:"sharing"`
|
||||||
// CustomCommands run shell commands on the selected item; see
|
// CustomCommands run shell commands on the selected item; see
|
||||||
// package commands.
|
// package commands.
|
||||||
CustomCommands []commands.Command `yaml:"customCommands"`
|
CustomCommands []commands.Command `yaml:"customCommands"`
|
||||||
@@ -68,6 +42,52 @@ type Config struct {
|
|||||||
Theme map[string][]string `yaml:"theme"`
|
Theme map[string][]string `yaml:"theme"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UI struct {
|
||||||
|
Icons *bool `yaml:"icons"`
|
||||||
|
// SourcesView is "grouped" (default) or "flat"; ` + "`" + ` toggles it.
|
||||||
|
SourcesView string `yaml:"sourcesView"`
|
||||||
|
// CollapseGroups starts the Sources pane with its groups folded;
|
||||||
|
// defaults to true.
|
||||||
|
CollapseGroups *bool `yaml:"collapseGroups"`
|
||||||
|
// LoadDelay is how long the cursor must rest on a row before its
|
||||||
|
// contents load; 0s loads at once. Defaults to 150ms.
|
||||||
|
LoadDelay *time.Duration `yaml:"loadDelay"`
|
||||||
|
// ExpiryWarningDays marks secrets, keys and certificates expiring
|
||||||
|
// within this many days; 0 marks only expired ones. Defaults to 30.
|
||||||
|
ExpiryWarningDays *int `yaml:"expiryWarningDays"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Values struct {
|
||||||
|
// RevealTimeout hides a revealed value after this long; 0s never.
|
||||||
|
// Defaults to 60s.
|
||||||
|
RevealTimeout *time.Duration `yaml:"revealTimeout"`
|
||||||
|
// HideOnBlur hides a revealed value when the terminal loses focus.
|
||||||
|
// Defaults to true.
|
||||||
|
HideOnBlur *bool `yaml:"hideOnBlur"`
|
||||||
|
ClipboardClear *time.Duration `yaml:"clipboardClear"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Storage struct {
|
||||||
|
// Cache keeps what was loaded (never values) between runs, so lists
|
||||||
|
// show at once and refresh in the background; defaults to true.
|
||||||
|
Cache *bool `yaml:"cache"`
|
||||||
|
// History counts uses of items for the Recent list; defaults to true.
|
||||||
|
History *bool `yaml:"history"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Updates struct {
|
||||||
|
// AutoCheck looks for a newer release in the background at startup,
|
||||||
|
// at most once per AutoCheckInterval; defaults to true. U and
|
||||||
|
// lazysecrets update work either way.
|
||||||
|
AutoCheck *bool `yaml:"autoCheck"`
|
||||||
|
// AutoCheckInterval defaults to 24h.
|
||||||
|
AutoCheckInterval *time.Duration `yaml:"autoCheckInterval"`
|
||||||
|
// UpdateSources are the repositories updates come from, tried in
|
||||||
|
// order: on GitHub, or a Gitea or Forgejo server. Defaults to GitHub,
|
||||||
|
// then git.kebcloud.no (update.DefaultServers).
|
||||||
|
UpdateSources []string `yaml:"updateSources"`
|
||||||
|
}
|
||||||
|
|
||||||
type LinkHandler struct {
|
type LinkHandler struct {
|
||||||
// Terminal to open links in, overriding the one handler install
|
// Terminal to open links in, overriding the one handler install
|
||||||
// detected: kitty, ghostty, wezterm, iterm2, terminal, alacritty,
|
// detected: kitty, ghostty, wezterm, iterm2, terminal, alacritty,
|
||||||
@@ -85,7 +105,7 @@ type LinkHandler struct {
|
|||||||
Prompt *bool `yaml:"prompt"`
|
Prompt *bool `yaml:"prompt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Share struct {
|
type Sharing struct {
|
||||||
// Key is the secret whose value encrypts shared files (its
|
// Key is the secret whose value encrypts shared files (its
|
||||||
// lazysecrets:// link, Key Vault URL or reference), for repos whose
|
// lazysecrets:// link, Key Vault URL or reference), for repos whose
|
||||||
// .lazysecrets.yaml doesn't name one.
|
// .lazysecrets.yaml doesn't name one.
|
||||||
@@ -103,15 +123,20 @@ type KeyRules struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Providers struct {
|
type Providers struct {
|
||||||
Azure Azure `yaml:"azure"`
|
// Timeout bounds each backend request; defaults to 30s. Each
|
||||||
Databricks Databricks `yaml:"databricks"`
|
// provider's own timeout overrides it for that provider.
|
||||||
Repos Repos `yaml:"repos"`
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
|
Azure Azure `yaml:"azure"`
|
||||||
|
Databricks Databricks `yaml:"databricks"`
|
||||||
|
Repos Repos `yaml:"repos"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Repos are git repositories' secret files (SOPS, .env). Inside a repo,
|
// Repos are git repositories' secret files (SOPS, .env). Inside a repo,
|
||||||
// that repo is the only one shown; elsewhere, the ones in Paths and under
|
// that repo is the only one shown; elsewhere, the ones in Paths and under
|
||||||
// Roots.
|
// Roots.
|
||||||
type Repos struct {
|
type Repos struct {
|
||||||
|
// Timeout overrides providers.timeout for these requests.
|
||||||
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
// Enabled defaults to true.
|
// Enabled defaults to true.
|
||||||
Enabled *bool `yaml:"enabled"`
|
Enabled *bool `yaml:"enabled"`
|
||||||
Paths []string `yaml:"paths"`
|
Paths []string `yaml:"paths"`
|
||||||
@@ -126,6 +151,8 @@ type Repos struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Azure struct {
|
type Azure struct {
|
||||||
|
// Timeout overrides providers.timeout for these requests.
|
||||||
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
// Enabled defaults to true.
|
// Enabled defaults to true.
|
||||||
Enabled *bool `yaml:"enabled"`
|
Enabled *bool `yaml:"enabled"`
|
||||||
// HideSubscriptions lists subscriptions to leave out, by name or ID.
|
// HideSubscriptions lists subscriptions to leave out, by name or ID.
|
||||||
@@ -136,6 +163,8 @@ type Azure struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Databricks struct {
|
type Databricks struct {
|
||||||
|
// Timeout overrides providers.timeout for these requests.
|
||||||
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
// Enabled defaults to true.
|
// Enabled defaults to true.
|
||||||
Enabled *bool `yaml:"enabled"`
|
Enabled *bool `yaml:"enabled"`
|
||||||
HideProfiles []string `yaml:"hideProfiles"`
|
HideProfiles []string `yaml:"hideProfiles"`
|
||||||
@@ -222,35 +251,38 @@ func decodeInto(c *Config, path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c Config) validate(path string) error {
|
func (c Config) validate(path string) error {
|
||||||
if v := c.SourcesView; v != "" && v != "grouped" && v != "flat" {
|
if v := c.UI.SourcesView; v != "" && v != "grouped" && v != "flat" {
|
||||||
return fmt.Errorf("%s: sourcesView: %q is not grouped or flat", path, v)
|
return fmt.Errorf("%s: ui.sourcesView: %q is not grouped or flat", path, v)
|
||||||
}
|
}
|
||||||
for _, cmd := range c.CustomCommands {
|
for _, cmd := range c.CustomCommands {
|
||||||
if err := cmd.Validate(); err != nil {
|
if err := cmd.Validate(); err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if d := c.ExpiryWarningDays; d != nil && *d < 0 {
|
if d := c.UI.ExpiryWarningDays; d != nil && *d < 0 {
|
||||||
return fmt.Errorf("%s: expiryWarningDays: %d is negative", path, *d)
|
return fmt.Errorf("%s: ui.expiryWarningDays: %d is negative", path, *d)
|
||||||
}
|
}
|
||||||
if d := c.LoadDelay; d != nil && (*d < 0 || *d > 5*time.Second) {
|
if d := c.UI.LoadDelay; d != nil && (*d < 0 || *d > 5*time.Second) {
|
||||||
return fmt.Errorf("%s: loadDelay: %s is not between 0s and 5s", path, *d)
|
return fmt.Errorf("%s: ui.loadDelay: %s is not between 0s and 5s", path, *d)
|
||||||
}
|
}
|
||||||
for i, r := range c.Releases {
|
if d := c.Updates.AutoCheckInterval; d != nil && *d <= 0 {
|
||||||
|
return fmt.Errorf("%s: updates.autoCheckInterval: %s is not positive (autoCheck: false turns it off)", path, *d)
|
||||||
|
}
|
||||||
|
for i, r := range c.Updates.UpdateSources {
|
||||||
if _, err := update.API(r); err != nil || !strings.HasPrefix(r, "https://") {
|
if _, err := update.API(r); err != nil || !strings.HasPrefix(r, "https://") {
|
||||||
return fmt.Errorf("%s: releases[%d]: %q is not an https:// repository address, like %s", path, i, r, update.GitHubRepo)
|
return fmt.Errorf("%s: updates.updateSources[%d]: %q is not an https:// repository address, like %s", path, i, r, update.GitHubRepo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if k := c.Share.Key; k != "" {
|
if k := c.Sharing.Key; k != "" {
|
||||||
if _, err := share.ParseKey(k); err != nil {
|
if _, err := share.ParseKey(k); err != nil {
|
||||||
return fmt.Errorf("%s: share.key: %w", path, err)
|
return fmt.Errorf("%s: sharing.key: %w", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if n := c.Share.KeyRules.MinLength; n != 0 && n < 16 {
|
if n := c.Sharing.KeyRules.MinLength; n != 0 && n < 16 {
|
||||||
return fmt.Errorf("%s: share.keyRules.minLength: %d is under 16", path, n)
|
return fmt.Errorf("%s: sharing.keyRules.minLength: %d is under 16", path, n)
|
||||||
}
|
}
|
||||||
if n := c.Share.KeyRules.MinClasses; n < 0 || n > 4 {
|
if n := c.Sharing.KeyRules.MinClasses; n < 0 || n > 4 {
|
||||||
return fmt.Errorf("%s: share.keyRules.minClasses: %d is not between 1 and 4", path, n)
|
return fmt.Errorf("%s: sharing.keyRules.minClasses: %d is not between 1 and 4", path, n)
|
||||||
}
|
}
|
||||||
for i, v := range c.Providers.Azure.Vaults {
|
for i, v := range c.Providers.Azure.Vaults {
|
||||||
if !strings.HasPrefix(v, "https://") {
|
if !strings.HasPrefix(v, "https://") {
|
||||||
|
|||||||
@@ -19,12 +19,17 @@ func write(t *testing.T, content string) string {
|
|||||||
|
|
||||||
func TestLoad(t *testing.T) {
|
func TestLoad(t *testing.T) {
|
||||||
c, err := Load(write(t, `
|
c, err := Load(write(t, `
|
||||||
icons: false
|
ui:
|
||||||
sourcesView: flat
|
icons: false
|
||||||
clipboardClear: 45s
|
sourcesView: flat
|
||||||
timeout: 1m
|
values:
|
||||||
|
clipboardClear: 45s
|
||||||
|
storage:
|
||||||
|
history: false
|
||||||
providers:
|
providers:
|
||||||
|
timeout: 1m
|
||||||
azure:
|
azure:
|
||||||
|
timeout: 2m
|
||||||
hideSubscriptions: [platform-mgmt]
|
hideSubscriptions: [platform-mgmt]
|
||||||
vaults:
|
vaults:
|
||||||
- https://kv-foo.vault.azure.net/
|
- https://kv-foo.vault.azure.net/
|
||||||
@@ -35,12 +40,12 @@ providers:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if c.Icons == nil || *c.Icons || c.ClipboardClear == nil || *c.ClipboardClear != 45*time.Second ||
|
if c.UI.Icons == nil || *c.UI.Icons || c.Values.ClipboardClear == nil || *c.Values.ClipboardClear != 45*time.Second ||
|
||||||
c.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)
|
t.Errorf("config = %+v", c)
|
||||||
}
|
}
|
||||||
if c.SourcesView != "flat" {
|
if c.UI.SourcesView != "flat" || On(c.Storage.History) || !On(c.Storage.Cache) {
|
||||||
t.Errorf("sourcesView = %q", c.SourcesView)
|
t.Errorf("ui = %+v, storage = %+v", c.UI, c.Storage)
|
||||||
}
|
}
|
||||||
if !On(c.Providers.Azure.Enabled) || On(c.Providers.Databricks.Enabled) {
|
if !On(c.Providers.Azure.Enabled) || On(c.Providers.Databricks.Enabled) {
|
||||||
t.Error("enabled flags wrong")
|
t.Error("enabled flags wrong")
|
||||||
@@ -57,7 +62,7 @@ func TestLoadMissingAndEmpty(t *testing.T) {
|
|||||||
"empty": write(t, ""),
|
"empty": write(t, ""),
|
||||||
} {
|
} {
|
||||||
c, err := Load(path)
|
c, err := Load(path)
|
||||||
if err != nil || c.Icons != nil || c.ClipboardClear != nil {
|
if err != nil || c.UI.Icons != nil || c.Values.ClipboardClear != nil {
|
||||||
t.Errorf("%s: %+v, %v", name, c, err)
|
t.Errorf("%s: %+v, %v", name, c, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,14 +73,16 @@ func TestLoadRejectsMistakes(t *testing.T) {
|
|||||||
"typo": "icon: true\n",
|
"typo": "icon: true\n",
|
||||||
"bad vault": "providers:\n azure:\n vaults: [kv-foo]\n",
|
"bad vault": "providers:\n azure:\n vaults: [kv-foo]\n",
|
||||||
"old layout": "azure:\n vaults: [https://kv.vault.azure.net/]\n",
|
"old layout": "azure:\n vaults: [https://kv.vault.azure.net/]\n",
|
||||||
"duration": "timeout: soon\n",
|
"flat keys": "icons: false\n",
|
||||||
"view": "sourcesView: tree\n",
|
"duration": "providers:\n timeout: soon\n",
|
||||||
"releases": "releases: [git.example.com/team/lazysecrets]\n",
|
"view": "ui:\n sourcesView: tree\n",
|
||||||
"http": "releases: [http://git.example.com/team/lazysecrets]\n",
|
"sources": "updates:\n updateSources: [git.example.com/team/lazysecrets]\n",
|
||||||
"share key": "share:\n key: https://kv.vault.azure.net/\n",
|
"http": "updates:\n updateSources: [http://git.example.com/team/lazysecrets]\n",
|
||||||
"workspace": "share:\n key: \"{{secrets/team/env-share}}\"\n",
|
"interval": "updates:\n autoCheckInterval: 0s\n",
|
||||||
"short keys": "share:\n keyRules:\n minLength: 8\n",
|
"share key": "sharing:\n key: https://kv.vault.azure.net/\n",
|
||||||
"classes": "share:\n keyRules:\n minClasses: 5\n",
|
"workspace": "sharing:\n key: \"{{secrets/team/env-share}}\"\n",
|
||||||
|
"short keys": "sharing:\n keyRules:\n minLength: 8\n",
|
||||||
|
"classes": "sharing:\n keyRules:\n minClasses: 5\n",
|
||||||
} {
|
} {
|
||||||
if _, err := Load(write(t, content)); err == nil {
|
if _, err := Load(write(t, content)); err == nil {
|
||||||
t.Errorf("%s: no error", name)
|
t.Errorf("%s: no error", name)
|
||||||
@@ -87,7 +94,8 @@ func TestLoadRejectsMistakes(t *testing.T) {
|
|||||||
|
|
||||||
func TestLoadAllLayers(t *testing.T) {
|
func TestLoadAllLayers(t *testing.T) {
|
||||||
user := write(t, `
|
user := write(t, `
|
||||||
icons: false
|
ui:
|
||||||
|
icons: false
|
||||||
theme:
|
theme:
|
||||||
azureColor: [blue]
|
azureColor: [blue]
|
||||||
errorColor: [red, bold]
|
errorColor: [red, bold]
|
||||||
@@ -96,6 +104,8 @@ providers:
|
|||||||
hideSubscriptions: [a]
|
hideSubscriptions: [a]
|
||||||
`)
|
`)
|
||||||
theme := write(t, `
|
theme := write(t, `
|
||||||
|
ui:
|
||||||
|
sourcesView: flat
|
||||||
theme:
|
theme:
|
||||||
azureColor: ["#82aaff"]
|
azureColor: ["#82aaff"]
|
||||||
activeBorderColor: ["#ff966c", bold]
|
activeBorderColor: ["#ff966c", bold]
|
||||||
@@ -104,7 +114,8 @@ theme:
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if c.Icons == nil || *c.Icons || len(c.Providers.Azure.HideSubscriptions) != 1 {
|
// A group set in both files merges: icons from one, sourcesView the other.
|
||||||
|
if c.UI.Icons == nil || *c.UI.Icons || c.UI.SourcesView != "flat" || len(c.Providers.Azure.HideSubscriptions) != 1 {
|
||||||
t.Errorf("user settings lost: %+v", c)
|
t.Errorf("user settings lost: %+v", c)
|
||||||
}
|
}
|
||||||
want := map[string][]string{
|
want := map[string][]string{
|
||||||
@@ -119,19 +130,19 @@ theme:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReleases(t *testing.T) {
|
func TestUpdates(t *testing.T) {
|
||||||
c, err := Load(write(t, "releases: [https://git.example.com/team/lazysecrets]\n"))
|
c, err := Load(write(t, "updates:\n autoCheck: false\n autoCheckInterval: 168h\n updateSources: [https://git.example.com/team/lazysecrets]\n"))
|
||||||
if err != nil || len(c.Releases) != 1 {
|
if err != nil || On(c.Updates.AutoCheck) || *c.Updates.AutoCheckInterval != 7*24*time.Hour || len(c.Updates.UpdateSources) != 1 {
|
||||||
t.Errorf("releases = %v, %v", c.Releases, err)
|
t.Errorf("updates = %+v, %v", c.Updates, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadDelay(t *testing.T) {
|
func TestLoadDelay(t *testing.T) {
|
||||||
c, err := Load(write(t, "loadDelay: 300ms\n"))
|
c, err := Load(write(t, "ui:\n loadDelay: 300ms\n"))
|
||||||
if err != nil || c.LoadDelay == nil || *c.LoadDelay != 300*time.Millisecond {
|
if err != nil || c.UI.LoadDelay == nil || *c.UI.LoadDelay != 300*time.Millisecond {
|
||||||
t.Errorf("loadDelay = %v, %v", c.LoadDelay, err)
|
t.Errorf("loadDelay = %v, %v", c.UI.LoadDelay, err)
|
||||||
}
|
}
|
||||||
if _, err := Load(write(t, "loadDelay: 1m\n")); err == nil {
|
if _, err := Load(write(t, "ui:\n loadDelay: 1m\n")); err == nil {
|
||||||
t.Error("a minute's delay accepted")
|
t.Error("a minute's delay accepted")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -599,11 +599,11 @@ func normalizeRemote(remote string) (string, string) {
|
|||||||
type lazysecretsFile struct {
|
type lazysecretsFile struct {
|
||||||
Include []string `yaml:"include"`
|
Include []string `yaml:"include"`
|
||||||
Exclude []string `yaml:"exclude"`
|
Exclude []string `yaml:"exclude"`
|
||||||
Share struct {
|
Sharing struct {
|
||||||
// Key is the link to the secret that encrypts the repo's
|
// Key is the link to the secret that encrypts the repo's
|
||||||
// shared env files (package share).
|
// shared env files (package share).
|
||||||
Key string `yaml:"key"`
|
Key string `yaml:"key"`
|
||||||
} `yaml:"share"`
|
} `yaml:"sharing"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShareKey is the share.key of the repo's .lazysecrets.yaml, if any.
|
// ShareKey is the share.key of the repo's .lazysecrets.yaml, if any.
|
||||||
@@ -619,7 +619,7 @@ func ShareKey(root string) (string, error) {
|
|||||||
if err := yaml.Unmarshal(b, &f); err != nil {
|
if err := yaml.Unmarshal(b, &f); err != nil {
|
||||||
return "", fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err)
|
return "", fmt.Errorf("%s/.lazysecrets.yaml: %w", root, err)
|
||||||
}
|
}
|
||||||
return f.Share.Key, nil
|
return f.Sharing.Key, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// selector says which files to look at: user (the include/exclude of the
|
// selector says which files to look at: user (the include/exclude of the
|
||||||
|
|||||||
@@ -243,7 +243,7 @@ func TestDefaultKey(t *testing.T) {
|
|||||||
if err != nil || !ok || k.Name != "env-share" {
|
if err != nil || !ok || k.Name != "env-share" {
|
||||||
t.Errorf("general: %+v %v %v", k, ok, err)
|
t.Errorf("general: %+v %v %v", k, ok, err)
|
||||||
}
|
}
|
||||||
os.WriteFile(filepath.Join(dir, ".lazysecrets.yaml"), []byte("share:\n key: lazysecrets://azure/kv-app.vault.azure.net/secrets/app-share\n"), 0o644)
|
os.WriteFile(filepath.Join(dir, ".lazysecrets.yaml"), []byte("sharing:\n key: lazysecrets://azure/kv-app.vault.azure.net/secrets/app-share\n"), 0o644)
|
||||||
if k, _, _ := DefaultKey(dir, general); k.Name != "app-share" {
|
if k, _, _ := DefaultKey(dir, general); k.Name != "app-share" {
|
||||||
t.Errorf("the repo's should win: %+v", k)
|
t.Errorf("the repo's should win: %+v", k)
|
||||||
}
|
}
|
||||||
|
|||||||
+39
-16
@@ -65,6 +65,8 @@ const (
|
|||||||
type Options struct {
|
type Options struct {
|
||||||
// Timeout bounds every provider call. Defaults to 30s.
|
// Timeout bounds every provider call. Defaults to 30s.
|
||||||
Timeout time.Duration
|
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 shows Nerd Font glyphs next to backends.
|
||||||
Icons bool
|
Icons bool
|
||||||
// Clipboard defaults to the system clipboard, or OSC52 over SSH.
|
// Clipboard defaults to the system clipboard, or OSC52 over SSH.
|
||||||
@@ -117,9 +119,11 @@ type Options struct {
|
|||||||
// Keys, if set, replaces the default keymap (keys.Map.Apply).
|
// Keys, if set, replaces the default keymap (keys.Map.Apply).
|
||||||
Keys *keys.Map
|
Keys *keys.Map
|
||||||
// Updater, if set, finds and installs newer releases (U); with
|
// Updater, if set, finds and installs newer releases (U); with
|
||||||
// UpdateCheck, lazysecrets also looks once a day on its own.
|
// UpdateCheck, lazysecrets also looks on its own at startup, at most
|
||||||
|
// once per UpdateEvery (0 for a day).
|
||||||
Updater *Updater
|
Updater *Updater
|
||||||
UpdateCheck bool
|
UpdateCheck bool
|
||||||
|
UpdateEvery time.Duration
|
||||||
// HandlerSetup, if set, offers to set up the link handler at startup.
|
// HandlerSetup, if set, offers to set up the link handler at startup.
|
||||||
HandlerSetup *HandlerSetup
|
HandlerSetup *HandlerSetup
|
||||||
// NoHistory stops counting uses of items for the Recent list.
|
// NoHistory stops counting uses of items for the Recent list.
|
||||||
@@ -144,7 +148,7 @@ type Clipboard interface {
|
|||||||
type Model struct {
|
type Model struct {
|
||||||
providers []provider.Provider
|
providers []provider.Provider
|
||||||
byID map[string]provider.Provider
|
byID map[string]provider.Provider
|
||||||
timeout time.Duration
|
timeout timeouts
|
||||||
|
|
||||||
keys keys.Map
|
keys keys.Map
|
||||||
st styles.Styles
|
st styles.Styles
|
||||||
@@ -236,6 +240,7 @@ type Model struct {
|
|||||||
|
|
||||||
updater *Updater
|
updater *Updater
|
||||||
updateCheck bool
|
updateCheck bool
|
||||||
|
updateEvery time.Duration
|
||||||
fetchOnStart bool // look for a newer release in Init
|
fetchOnStart bool // look for a newer release in Init
|
||||||
newer *update.Release // a newer release to offer
|
newer *update.Release // a newer release to offer
|
||||||
updated string // the release installed, waiting for a restart
|
updated string // the release installed, waiting for a restart
|
||||||
@@ -375,7 +380,7 @@ func New(providers []provider.Provider, opts Options) Model {
|
|||||||
m := Model{
|
m := Model{
|
||||||
providers: providers,
|
providers: providers,
|
||||||
byID: map[string]provider.Provider{},
|
byID: map[string]provider.Provider{},
|
||||||
timeout: opts.Timeout,
|
timeout: timeouts{def: opts.Timeout, byKind: opts.Timeouts},
|
||||||
keys: keys.Default(),
|
keys: keys.Default(),
|
||||||
st: styles.Default(),
|
st: styles.Default(),
|
||||||
spin: spinner.New(spinner.WithSpinner(spinner.Dot)),
|
spin: spinner.New(spinner.WithSpinner(spinner.Dot)),
|
||||||
@@ -427,7 +432,10 @@ func New(providers []provider.Provider, opts Options) Model {
|
|||||||
m.revealTimeout, m.hideOnBlur = opts.RevealTimeout, opts.HideOnBlur
|
m.revealTimeout, m.hideOnBlur = opts.RevealTimeout, opts.HideOnBlur
|
||||||
m.expiryWarning = opts.ExpiryWarning
|
m.expiryWarning = opts.ExpiryWarning
|
||||||
m.handlerSetup = opts.HandlerSetup
|
m.handlerSetup = opts.HandlerSetup
|
||||||
m.updater, m.updateCheck = opts.Updater, opts.UpdateCheck
|
m.updater, m.updateCheck, m.updateEvery = opts.Updater, opts.UpdateCheck, opts.UpdateEvery
|
||||||
|
if m.updateEvery == 0 {
|
||||||
|
m.updateEvery = updateEvery
|
||||||
|
}
|
||||||
m.startUpdateCheck()
|
m.startUpdateCheck()
|
||||||
m.askHandler = opts.HandlerSetup != nil && !m.store.Flag(flagNoHandlerPrompt)
|
m.askHandler = opts.HandlerSetup != nil && !m.store.Flag(flagNoHandlerPrompt)
|
||||||
m.settle = opts.LoadDelay
|
m.settle = opts.LoadDelay
|
||||||
@@ -1240,7 +1248,7 @@ func (m *Model) showVersions(o provider.Object, key string) tea.Cmd {
|
|||||||
}
|
}
|
||||||
m.secrets.SetLoading()
|
m.secrets.SetLoading()
|
||||||
p := m.providerFor(o.Container.Source)
|
p := m.providerFor(o.Container.Source)
|
||||||
ctx, req := m.begin(slotObjects)
|
ctx, req := m.begin(slotObjects, o.Container.Source.Kind)
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
items, err := p.Versions(ctx, o)
|
items, err := p.Versions(ctx, o)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
@@ -1270,7 +1278,7 @@ func (m *Model) countVersions(o provider.Object) tea.Cmd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
p := m.providerFor(o.Container.Source)
|
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 {
|
return m.settled(ctx, func() tea.Msg {
|
||||||
items, err := p.Versions(ctx, o)
|
items, err := p.Versions(ctx, o)
|
||||||
for i := range items {
|
for i := range items {
|
||||||
@@ -1421,11 +1429,26 @@ func (m *Model) autoLoad(msg sourcesMsg) tea.Cmd {
|
|||||||
return m.crawlStart()
|
return m.crawlStart()
|
||||||
}
|
}
|
||||||
|
|
||||||
// begin starts a cancellable request for pane p, cancelling any in flight.
|
// timeouts bound each provider request: its kind's, else the default.
|
||||||
func (m *Model) begin(p slot) (context.Context, int) {
|
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.cancelReq(p)
|
||||||
m.seq++
|
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
|
m.reqID[p], m.cancel[p] = m.seq, cancel
|
||||||
return ctx, m.seq
|
return ctx, m.seq
|
||||||
}
|
}
|
||||||
@@ -1479,7 +1502,7 @@ func (m Model) sourcesCmd() tea.Cmd {
|
|||||||
gen := m.sourcesGen
|
gen := m.sourcesGen
|
||||||
var cmds []tea.Cmd
|
var cmds []tea.Cmd
|
||||||
for _, p := range m.providers {
|
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 {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
s, err := p.Sources(ctx)
|
s, err := p.Sources(ctx)
|
||||||
@@ -1490,7 +1513,7 @@ func (m Model) sourcesCmd() tea.Cmd {
|
|||||||
})
|
})
|
||||||
// The source it started in, quickly, on its own.
|
// The source it started in, quickly, on its own.
|
||||||
if c, ok := p.(provider.Current); ok {
|
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 {
|
cmds = append(cmds, func() tea.Msg {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
src, ok := c.Current(ctx)
|
src, ok := c.Current(ctx)
|
||||||
@@ -1507,7 +1530,7 @@ func (m Model) sourcesCmd() tea.Cmd {
|
|||||||
|
|
||||||
func (m *Model) loadContainers(src provider.Source, key string) tea.Cmd {
|
func (m *Model) loadContainers(src provider.Source, key string) tea.Cmd {
|
||||||
p := m.providerFor(src)
|
p := m.providerFor(src)
|
||||||
ctx, req := m.begin(slotContainers)
|
ctx, req := m.begin(slotContainers, src.Kind)
|
||||||
return m.settled(ctx, func() tea.Msg {
|
return m.settled(ctx, func() tea.Msg {
|
||||||
items, err := p.Containers(ctx, src)
|
items, err := p.Containers(ctx, src)
|
||||||
// Carry the source as given, with its provider ID, so later
|
// Carry the source as given, with its provider ID, so later
|
||||||
@@ -1526,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 {
|
func (m *Model) loadObjects(c provider.Container, t provider.ObjectType, key string) tea.Cmd {
|
||||||
cands := m.candidates(c.Source)
|
cands := m.candidates(c.Source)
|
||||||
known := len(cands) == 1 && cands[0].ID() == c.Source.Provider
|
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 {
|
return m.settled(ctx, func() tea.Msg {
|
||||||
var errs []error
|
var errs []error
|
||||||
for _, p := range cands {
|
for _, p := range cands {
|
||||||
@@ -1554,7 +1577,7 @@ func (m *Model) loadObjects(c provider.Container, t provider.ObjectType, key str
|
|||||||
|
|
||||||
func (m *Model) fetchValue(s provider.Object) tea.Cmd {
|
func (m *Model) fetchValue(s provider.Object) tea.Cmd {
|
||||||
p := m.providerFor(s.Container.Source)
|
p := m.providerFor(s.Container.Source)
|
||||||
ctx, req := m.begin(slotValue)
|
ctx, req := m.begin(slotValue, s.Container.Source.Kind)
|
||||||
m.detail.SetLoading()
|
m.detail.SetLoading()
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
v, err := p.Value(ctx, s)
|
v, err := p.Value(ctx, s)
|
||||||
@@ -1573,7 +1596,7 @@ func (m *Model) describe(o provider.Object) tea.Cmd {
|
|||||||
m.detail.SetDescribing()
|
m.detail.SetDescribing()
|
||||||
}
|
}
|
||||||
p := m.providerFor(o.Container.Source)
|
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 {
|
return m.settled(ctx, func() tea.Msg {
|
||||||
obj, err := p.Describe(ctx, o)
|
obj, err := p.Describe(ctx, o)
|
||||||
return describeMsg{req: req, key: key, obj: obj, err: err}
|
return describeMsg{req: req, key: key, obj: obj, err: err}
|
||||||
@@ -1601,7 +1624,7 @@ func (m *Model) copyValue() tea.Cmd {
|
|||||||
return m.copyText(o.Name, v.Reveal(), true)
|
return m.copyText(o.Name, v.Reveal(), true)
|
||||||
}
|
}
|
||||||
p := m.providerFor(o.Container.Source)
|
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 + "…"}
|
m.flash = flash{text: "copying " + o.Name + "…"}
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
v, err := p.Value(ctx, o)
|
v, err := p.Value(ctx, o)
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func (m *Model) runCommand(c commands.Command, rendered string, obj *provider.Ob
|
|||||||
o := *obj
|
o := *obj
|
||||||
p := m.providerFor(o.Container.Source)
|
p := m.providerFor(o.Container.Source)
|
||||||
m.flash = flash{text: "fetching " + o.Name + " for " + c.Description + "…"}
|
m.flash = flash{text: "fetching " + o.Name + " for " + c.Description + "…"}
|
||||||
timeout := m.timeout
|
timeout := m.timeout.of(o.Container.Source.Kind)
|
||||||
m.usedObject(o)
|
m.usedObject(o)
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
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}
|
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 {
|
return m.settled(ctx, func() tea.Msg {
|
||||||
attrs, err := d.Details(ctx, c)
|
attrs, err := d.Details(ctx, c)
|
||||||
if ctx.Err() != nil {
|
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}
|
m.flash = flash{text: "copy: no account or profile for " + describeRef(r), isErr: true}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ctx, req := m.begin(slotCopy)
|
ctx, req := m.begin(slotCopy, r.Kind)
|
||||||
m.flash = flash{text: "copying " + r.Name + "…"}
|
m.flash = flash{text: "copying " + r.Name + "…"}
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|||||||
@@ -295,10 +295,10 @@ func (m *Model) crawlNext() tea.Cmd {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Model) crawlCmd(job crawlJob) tea.Cmd {
|
func (m *Model) crawlCmd(job crawlJob) tea.Cmd {
|
||||||
timeout := m.timeout
|
|
||||||
if job.src != nil {
|
if job.src != nil {
|
||||||
p := m.providerFor(*job.src)
|
p := m.providerFor(*job.src)
|
||||||
src := *job.src
|
src := *job.src
|
||||||
|
timeout := m.timeout.of(src.Kind)
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ctx, cancel := contextWithTimeout(timeout)
|
ctx, cancel := contextWithTimeout(timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -311,6 +311,7 @@ func (m *Model) crawlCmd(job crawlJob) tea.Cmd {
|
|||||||
}
|
}
|
||||||
c := *job.container
|
c := *job.container
|
||||||
p := m.providerFor(c.Source)
|
p := m.providerFor(c.Source)
|
||||||
|
timeout := m.timeout.of(c.Source.Kind)
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ctx, cancel := contextWithTimeout(timeout)
|
ctx, cancel := contextWithTimeout(timeout)
|
||||||
defer cancel()
|
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")
|
m.flash = warning("these files can't be shared")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
general, timeout := m.shareKey, m.timeout
|
general, timeout := m.shareKey, m.timeout.of(src.Kind)
|
||||||
m.flash = flash{text: "reading the files…"}
|
m.flash = flash{text: "reading the files…"}
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
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) {
|
func (m *Model) keyReader() func(ref.Ref) (provider.SecretValue, string, error) {
|
||||||
providers, st, timeout := m.providers, m.store, m.timeout
|
providers, st, timeout := m.providers, m.store, m.timeout
|
||||||
return func(key ref.Ref) (provider.SecretValue, string, error) {
|
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()
|
defer cancel()
|
||||||
k := key
|
k := key
|
||||||
k.Version = ""
|
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.
|
// openShare reads the key, decrypts the files and finds the repo's clone.
|
||||||
func (m *Model) openShare(s share.Sealed) tea.Cmd {
|
func (m *Model) openShare(s share.Sealed) tea.Cmd {
|
||||||
read := m.keyReader()
|
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 + "…"}
|
m.flash = flash{text: "decrypting with " + s.Key.Name + "…"}
|
||||||
return func() tea.Msg {
|
return func() tea.Msg {
|
||||||
msg := shareOpenedMsg{sealed: s}
|
msg := shareOpenedMsg{sealed: s}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func TestShareWithShareKeyConfirms(t *testing.T) {
|
|||||||
}
|
}
|
||||||
root := cloneWith(t, map[string]string{
|
root := cloneWith(t, map[string]string{
|
||||||
".env": "A=1\n",
|
".env": "A=1\n",
|
||||||
".lazysecrets.yaml": "share:\n key: https://" + kv + "/secrets/db-password\n",
|
".lazysecrets.yaml": "sharing:\n key: https://" + kv + "/secrets/db-password\n",
|
||||||
})
|
})
|
||||||
clip := &fakeClipboard{}
|
clip := &fakeClipboard{}
|
||||||
d := newDriverOpts(t, Options{Clipboard: clip, ShareRules: demoRules}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
|
d := newDriverOpts(t, Options{Clipboard: clip, ShareRules: demoRules}, fake.Azure(0), repo.New(repo.Settings{Dir: root}))
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,8 @@ type Updater struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// updateEvery is how often lazysecrets looks for a newer release on its
|
// updateEvery is how often lazysecrets looks for a newer release on its
|
||||||
// own; within that, the last answer (in the state file) is used.
|
// own unless the config says otherwise; within that, the last answer (in
|
||||||
|
// the state file) is used.
|
||||||
const updateEvery = 24 * time.Hour
|
const updateEvery = 24 * time.Hour
|
||||||
|
|
||||||
type updateMsg struct {
|
type updateMsg struct {
|
||||||
@@ -35,14 +36,14 @@ type installedMsg struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// startUpdateCheck sets up the startup look for a newer release: the
|
// startUpdateCheck sets up the startup look for a newer release: the
|
||||||
// state file's answer when it looked less than a day ago, else a quiet
|
// state file's answer when it looked within updateEvery, else a quiet
|
||||||
// look at the server (fetchOnStart, for Init).
|
// look at the server (fetchOnStart, for Init).
|
||||||
func (m *Model) startUpdateCheck() {
|
func (m *Model) startUpdateCheck() {
|
||||||
if m.updater == nil || !m.updateCheck {
|
if m.updater == nil || !m.updateCheck {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
last := m.store.UpdateCheck()
|
last := m.store.UpdateCheck()
|
||||||
if m.now().Sub(last.At) < updateEvery {
|
if m.now().Sub(last.At) < m.updateEvery {
|
||||||
m.offer(update.Release{Tag: last.Tag, Body: last.Notes, URL: last.URL})
|
m.offer(update.Release{Tag: last.Tag, Body: last.Notes, URL: last.URL})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,12 @@ func TestUpdate(t *testing.T) {
|
|||||||
if f2.asked != 1 {
|
if f2.asked != 1 {
|
||||||
t.Errorf("asked %d times a day later", f2.asked)
|
t.Errorf("asked %d times a day later", f2.asked)
|
||||||
}
|
}
|
||||||
|
// With autoCheckInterval: 1h, two hours on is long enough.
|
||||||
|
mem.SetUpdateCheck(store.UpdateCheck{At: time.Now().Add(-2 * time.Hour), Tag: "v0.2.0"})
|
||||||
|
newDriverOpts(t, Options{Version: "0.1.1", Updater: f2.updater(), UpdateCheck: true, UpdateEvery: time.Hour, Store: mem})
|
||||||
|
if f2.asked != 2 {
|
||||||
|
t.Errorf("asked %d times, want a look after the hour", f2.asked)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateAsked(t *testing.T) {
|
func TestUpdateAsked(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
// Usage: revealing, copying, opening in the browser, sharing and jumping
|
// Usage: revealing, copying, opening in the browser, sharing and jumping
|
||||||
// to an item count as using it. The store keeps when each item was last
|
// to an item count as using it. The store keeps when each item was last
|
||||||
// used and how often, for the Recent section of the jump list. The config's
|
// used and how often, for the Recent section of the jump list. The config's
|
||||||
// history: false turns this off.
|
// storage.history: false turns this off.
|
||||||
|
|
||||||
// used counts a use of the remembered item key, keeping its context.
|
// used counts a use of the remembered item key, keeping its context.
|
||||||
func (m *Model) used(key string) {
|
func (m *Model) used(key string) {
|
||||||
|
|||||||
Reference in new issue
Block a user