A password manager's CLI can prompt; with a lazy client it runs once, on the first question, instead of on every start.
- commit
- 710b5e6b52f1f4666a35faf4cc4f4c1e9bf68fcc
- parent
- c97c9c9d05a0cca615b5584df6c4782c52144b1e
- author
- Dami <dami.etoile@gmail.com>
| M | cmd/ | 17 | +++++++++++++++-- |
| M | cmd/ | 7 | +++++++ |
| M | internal/ | 26 | ++++++++++++++++++++++++++ |
| M | internal/ | 17 | +++++++++++++++++ |
@@ -130,14 +130,27 @@ func jevConfig(c *config.Config, key string) jev.Config {
130130 // startJEV finds the API key and connects, returning notes for the131131 // context line: why the key couldn't be read, or that a .env file's key132132 // is no longer read.133+//134+// jev-api-key-command runs only when the environment and the credential135+// store have no key, and then only when a sheet first asks JEV136+// something, so a password manager doesn't prompt on every start.133137 func startJEV(c *config.Config, e env, store keyring.Store) (jev.Client, []string) {134- ctx, cancel := context.WithTimeout(context.Background(), 2*jev.KeyCommandTimeout)138+ ctx, cancel := context.WithTimeout(context.Background(), jev.KeyCommandTimeout)135139 defer cancel()136- key, _, err := jev.ResolveKey(ctx, jev.KeySources{Getenv: e.getenv, Store: store, Command: c.String("jev-api-key-command")})140+ key, _, err := jev.ResolveKey(ctx, jev.KeySources{Getenv: e.getenv, Store: store})137141 var notes []string138142 if err != nil {139143 notes = append(notes, "JEV API key: "+err.Error())140144 }145+ if command := c.String("jev-api-key-command"); key == "" && command != "" {146+ return jev.Lazy(func() (jev.Client, error) {147+ key, _, err := jev.ResolveKey(context.Background(), jev.KeySources{Command: command})148+ if err != nil {149+ return nil, err150+ }151+ return jev.NewClient(jevConfig(c, key))152+ }), notes153+ }141154 if key == "" {142155 if jev.DotEnvHasKey(".") {143156 notes = append(notes, "012 no longer reads TYPESAFE_API_KEY from .env files: run 012 config set-key to store it")@@ -132,6 +132,13 @@ func TestStartJEV(t *testing.T) {
132132 t.Errorf(".env: %v %v", client, notes)133133 }134134 135+ // jev-api-key-command waits for the first question.136+ writeConfig(t, "jev-api-key-command = /nonexistent/key-helper\n")137+ c, _ = loadConfig(e, nil)138+ if client, notes := startJEV(c, e, store); client == nil || len(notes) != 0 {139+ t.Errorf("command: %v %v", client, notes)140+ }141+135142 // The base URL only comes from the config or environment, and must136143 // be https.137144 e.getenv = func(k string) string {@@ -10,8 +10,11 @@ import (
1010 "os/exec"1111 "path/filepath"1212 "strings"13+ "sync"1314 "time"1415 16+ typesafe "github.com/FelineStateMachine/typesafe-go"17+1518 "github.com/FelineStateMachine/012/internal/config"1619 "github.com/FelineStateMachine/012/internal/keyring"1720 )@@ -154,3 +157,26 @@ func LoadConfig(dirs ...string) (Config, bool) {
154157 }155158 return c, c.APIKey != ""156159 }160+161+// Lazy is a Client that connects the first time a question is asked,162+// with connect, and remembers the result, error included. 012 uses it for163+// jev-api-key-command, so a password manager is only asked for the key164+// when a sheet actually uses JEV.165+func Lazy(connect func() (Client, error)) Client {166+ return &lazyClient{connect: connect}167+}168+169+type lazyClient struct {170+ once sync.Once171+ connect func() (Client, error)172+ client Client173+ err error174+}175+176+func (l *lazyClient) SystemOne(ctx context.Context, r typesafe.SystemOneRequest) (*typesafe.SystemOneResponse, error) {177+ l.once.Do(func() { l.client, l.err = l.connect() })178+ if l.err != nil {179+ return nil, l.err180+ }181+ return l.client.SystemOne(ctx, r)182+}@@ -12,6 +12,7 @@ import (
1212 "time"1313 1414 "github.com/FelineStateMachine/012/internal/keyring"15+ "github.com/FelineStateMachine/012/internal/sheet"1516 )1617 1718 func env(vals map[string]string) func(string) string {@@ -150,3 +151,19 @@ func TestNewClientChecksBaseURL(t *testing.T) {
150151 t.Errorf("localhost: %v", err)151152 }152153 }154+155+func TestLazyConnectsOnce(t *testing.T) {156+ calls := 0157+ c := Lazy(func() (Client, error) { calls++; return nil, errors.New("jev-api-key-command: op: exit status 1") })158+ if calls != 0 {159+ t.Fatal("connected before a question")160+ }161+ for range 2 {162+ if a := Ask(context.Background(), c, sheet.RemoteCall{Kind: "noul", Instructions: "Q", Criteria: [2]string{}}); !strings.Contains(a.Failed, "exit status 1") {163+ t.Errorf("answer %+v", a)164+ }165+ }166+ if calls != 1 {167+ t.Errorf("connected %d times", calls)168+ }169+}