Skip to content

Commit 5241b46

Browse files
authored
Merge pull request #110 from euank/lazy-init
pass: only init on run, and do so lazily
2 parents 8502b53 + 3cba391 commit 5241b46

File tree

1 file changed

+47
-35
lines changed

1 file changed

+47
-35
lines changed

pass/pass_linux.go

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,73 @@ import (
1414
"os/exec"
1515
"path"
1616
"strings"
17+
"sync"
1718

1819
"github.com/docker/docker-credential-helpers/credentials"
1920
)
2021

2122
const PASS_FOLDER = "docker-credential-helpers"
2223

23-
var (
24-
PassInitialized bool
25-
)
24+
// Pass handles secrets using Linux secret-service as a store.
25+
type Pass struct{}
26+
27+
// Ideally these would be stored as members of Pass, but since all of Pass's
28+
// methods have value receivers, not pointer receivers, and changing that is
29+
// backwards incompatible, we assume that all Pass instances share the same configuration
30+
31+
// initializationMutex is held while initializing so that only one 'pass'
32+
// round-tripping is done to check pass is functioning.
33+
var initializationMutex sync.Mutex
34+
var passInitialized bool
35+
36+
// CheckInitialized checks whether the password helper can be used. It
37+
// internally caches and so may be safely called multiple times with no impact
38+
// on performance, though the first call may take longer.
39+
func (p Pass) CheckInitialized() bool {
40+
return p.checkInitialized() == nil
41+
}
2642

27-
func init() {
43+
func (p Pass) checkInitialized() error {
44+
initializationMutex.Lock()
45+
defer initializationMutex.Unlock()
46+
if passInitialized {
47+
return nil
48+
}
2849
// In principle, we could just run `pass init`. However, pass has a bug
2950
// where if gpg fails, it doesn't always exit 1. Additionally, pass
3051
// uses gpg2, but gpg is the default, which may be confusing. So let's
3152
// just explictily check that pass actually can store and retreive a
3253
// password.
3354
password := "pass is initialized"
34-
name := path.Join(PASS_FOLDER, "docker-pass-initialized-check")
55+
name := path.Join(getPassDir(), "docker-pass-initialized-check")
3556

36-
_, err := runPass(password, "insert", "-f", "-m", name)
57+
_, err := p.runPassHelper(password, "insert", "-f", "-m", name)
3758
if err != nil {
38-
return
59+
return fmt.Errorf("error initializing pass: %v", err)
3960
}
4061

41-
stored, err := runPass("", "show", name)
42-
PassInitialized = err == nil && stored == password
62+
stored, err := p.runPassHelper("", "show", name)
63+
if err != nil {
64+
return fmt.Errorf("error fetching password during initialization: %v", err)
65+
}
66+
if stored != password {
67+
return fmt.Errorf("error round-tripping password during initialization: %q != %q", password, stored)
68+
}
69+
passInitialized = true
70+
return nil
71+
}
4372

44-
if PassInitialized {
45-
runPass("", "rm", "-rf", name)
73+
func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
74+
if err := p.checkInitialized(); err != nil {
75+
return "", err
4676
}
77+
return p.runPassHelper(stdinContent, args...)
4778
}
4879

49-
func runPass(stdin string, args ...string) (string, error) {
80+
func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
5081
var stdout, stderr bytes.Buffer
5182
cmd := exec.Command("pass", args...)
52-
cmd.Stdin = strings.NewReader(stdin)
83+
cmd.Stdin = strings.NewReader(stdinContent)
5384
cmd.Stdout = &stdout
5485
cmd.Stderr = &stderr
5586

@@ -62,37 +93,26 @@ func runPass(stdin string, args ...string) (string, error) {
6293
return strings.TrimRight(stdout.String(), "\n\r"), nil
6394
}
6495

65-
// Pass handles secrets using Linux secret-service as a store.
66-
type Pass struct{}
67-
6896
// Add adds new credentials to the keychain.
6997
func (h Pass) Add(creds *credentials.Credentials) error {
70-
if !PassInitialized {
71-
return errors.New("pass store is uninitialized")
72-
}
73-
7498
if creds == nil {
7599
return errors.New("missing credentials")
76100
}
77101

78102
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
79103

80-
_, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
104+
_, err := h.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
81105
return err
82106
}
83107

84108
// Delete removes credentials from the store.
85109
func (h Pass) Delete(serverURL string) error {
86-
if !PassInitialized {
87-
return errors.New("pass store is uninitialized")
88-
}
89-
90110
if serverURL == "" {
91111
return errors.New("missing server url")
92112
}
93113

94114
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
95-
_, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
115+
_, err := h.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
96116
return err
97117
}
98118

@@ -124,10 +144,6 @@ func listPassDir(args ...string) ([]os.FileInfo, error) {
124144

125145
// Get returns the username and secret to use for a given registry server URL.
126146
func (h Pass) Get(serverURL string) (string, string, error) {
127-
if !PassInitialized {
128-
return "", "", errors.New("pass store is uninitialized")
129-
}
130-
131147
if serverURL == "" {
132148
return "", "", errors.New("missing server url")
133149
}
@@ -152,16 +168,12 @@ func (h Pass) Get(serverURL string) (string, string, error) {
152168
}
153169

154170
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
155-
secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
171+
secret, err := h.runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
156172
return actual, secret, err
157173
}
158174

159175
// List returns the stored URLs and corresponding usernames for a given credentials label
160176
func (h Pass) List() (map[string]string, error) {
161-
if !PassInitialized {
162-
return nil, errors.New("pass store is uninitialized")
163-
}
164-
165177
servers, err := listPassDir()
166178
if err != nil {
167179
return nil, err

0 commit comments

Comments
 (0)