Skip to content

Commit bc238cf

Browse files
authored
fix: improve creds helper UX (runfinch#673)
Issue #, if available: *Description of changes:* - Make it so aws configure is also run on the run command, since that may invoke a pull - Clean up code and consider `config.json`'s state when checking if the cred helper is "installed" or not - TODO: remove need for var overriding to facilitate testing *Testing done:* - local testing, e2e tests - [x] I've reviewed the guidance in CONTRIBUTING.md #### License Acceptance By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Signed-off-by: Justin Alvarez <[email protected]>
1 parent 2c97323 commit bc238cf

File tree

3 files changed

+185
-65
lines changed

3 files changed

+185
-65
lines changed

Diff for: cmd/finch/nerdctl.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,15 @@ func (nc *nerdctlCommand) run(cmdName string, args []string) error {
173173

174174
var additionalEnv []string
175175
switch cmdName {
176-
case "build", "pull", "push":
176+
case "image":
177+
if slices.Contains(args, "build") || slices.Contains(args, "pull") || slices.Contains(args, "push") {
178+
ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger)
179+
}
180+
case "container":
181+
if slices.Contains(args, "run") {
182+
ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger)
183+
}
184+
case "build", "pull", "push", "run":
177185
ensureRemoteCredentials(nc.fc, nc.ecc, &additionalEnv, nc.logger)
178186
}
179187

Diff for: pkg/dependency/credhelper/cred_helper_binary.go

+126-52
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"encoding/json"
88
"fmt"
99
"os"
10+
"path/filepath"
1011
"strings"
1112

1213
"github.com/opencontainers/go-digest"
@@ -49,7 +50,7 @@ func newCredHelperBinary(fp path.Finch, fs afero.Fs, cmdCreator command.Creator,
4950

5051
// updateConfigFile updates the config.json file to configure the credential helper.
5152
func updateConfigFile(bin *credhelperbin) error {
52-
cfgPath := fmt.Sprintf("%s%s", bin.hcfg.finchPath, "config.json")
53+
cfgPath := filepath.Join(bin.hcfg.finchPath, "config.json")
5354
binCfgName := bin.credHelperConfigName()
5455
fileExists, err := afero.Exists(bin.fs, cfgPath)
5556
if err != nil {
@@ -71,6 +72,7 @@ func updateConfigFile(bin *credhelperbin) error {
7172
if err != nil {
7273
return err
7374
}
75+
defer fileRead.Close() //nolint:errcheck // closing the file
7476
bytes, err := afero.ReadAll(fileRead)
7577
if err != nil {
7678
return err
@@ -81,8 +83,7 @@ func updateConfigFile(bin *credhelperbin) error {
8183
return err
8284
}
8385
credsStore := cfg.CredentialsStore
84-
defer fileRead.Close() //nolint:errcheck // closing the file
85-
if strings.Compare(credsStore, binCfgName) != 0 {
86+
if credsStore != binCfgName {
8687
file, err := bin.fs.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
8788
if err != nil {
8889
return err
@@ -103,6 +104,38 @@ func updateConfigFile(bin *credhelperbin) error {
103104
return nil
104105
}
105106

107+
func (bin *credhelperbin) binaryInstalled() (bool, error) {
108+
return binaryInstalled(bin)
109+
}
110+
111+
func (bin *credhelperbin) configFileInstalled() (bool, error) {
112+
cfgPath := filepath.Join(bin.hcfg.finchPath, "config.json")
113+
binCfgName := bin.credHelperConfigName()
114+
115+
if fileExists, err := afero.Exists(bin.fs, cfgPath); err != nil {
116+
return false, err
117+
} else if !fileExists {
118+
return false, nil
119+
}
120+
121+
fileRead, err := bin.fs.Open(cfgPath)
122+
if err != nil {
123+
return false, err
124+
}
125+
bytes, err := afero.ReadAll(fileRead)
126+
if err != nil {
127+
return false, err
128+
}
129+
var cfg configfile.ConfigFile
130+
err = json.Unmarshal(bytes, &cfg)
131+
if err != nil {
132+
return false, err
133+
}
134+
credsStore := cfg.CredentialsStore
135+
defer fileRead.Close() //nolint:errcheck // closing the file
136+
return credsStore == binCfgName, nil
137+
}
138+
106139
// credHelperConfigName returns the name of the credential helper binary that will be used
107140
// inside the config.json.
108141
func (bin *credhelperbin) credHelperConfigName() string {
@@ -111,85 +144,126 @@ func (bin *credhelperbin) credHelperConfigName() string {
111144

112145
// fullInstallPath returns the full installation path of the credential helper binary.
113146
func (bin *credhelperbin) fullInstallPath() string {
114-
return fmt.Sprintf("%s%s", bin.hcfg.installFolder, bin.hcfg.binaryName)
147+
return filepath.Join(bin.hcfg.installFolder, bin.hcfg.binaryName)
115148
}
116149

117150
// Installed checks if the credential helper already exists in the specified
118151
// folder and checks if the hash of the installed binary is correct.
119152
func (bin *credhelperbin) Installed() bool {
120-
dirExists, err := afero.DirExists(bin.fs, bin.hcfg.installFolder)
121-
if err != nil {
122-
bin.l.Errorf("failed to get status of credential helper directory: %v", err)
123-
return false
124-
}
125-
if !dirExists {
126-
return false
127-
}
128-
fileExists, err := afero.Exists(bin.fs, bin.fullInstallPath())
129-
if err != nil {
130-
bin.l.Errorf("failed to get status of credential helper binary: %v", err)
131-
return false
132-
}
133-
if !fileExists {
134-
return false
135-
}
136-
file, err := bin.fs.Open(bin.fullInstallPath())
137-
if err != nil {
153+
if binInstalled, err := bin.binaryInstalled(); err != nil {
138154
bin.l.Error(err)
139155
return false
156+
} else if !binInstalled {
157+
return false
140158
}
141-
defer file.Close() //nolint:errcheck // closing the file
142-
hash, err := digest.FromReader(file)
143-
if err != nil {
159+
160+
if cfgInstalled, err := bin.configFileInstalled(); err != nil {
144161
bin.l.Error(err)
145162
return false
146-
}
147-
if strings.Compare(hash.String(), bin.hcfg.hash) != 0 {
148-
bin.l.Info("Hash of the installed credential helper binary does not match")
149-
err := bin.fs.Remove(bin.fullInstallPath())
150-
if err != nil {
151-
bin.l.Error(err)
152-
}
163+
} else if !cfgInstalled {
153164
return false
154165
}
166+
155167
return true
156168
}
157169

158170
// Install installs and configures the specified credential helper.
159171
func (bin *credhelperbin) Install() error {
160-
if bin.helper == "" {
161-
return nil
162-
}
163-
if strings.Compare(bin.helper, bin.credHelperConfigName()) != 0 {
164-
return nil
165-
}
166-
credHelperName := strings.ReplaceAll(bin.credHelperConfigName(), "-login", "")
167-
bin.l.Infof("Installing %s credential helper", credHelperName)
168-
mkdirCmd := bin.cmdCreator.Create("mkdir", "-p", bin.hcfg.installFolder)
169-
_, err := mkdirCmd.Output()
172+
binInstalled, err := bin.binaryInstalled()
170173
if err != nil {
171-
return fmt.Errorf("error creating installation directory %s, err: %w", bin.hcfg.installFolder, err)
174+
return err
172175
}
173176

174-
curlCmd := bin.cmdCreator.Create("curl", "--retry", "5", "--retry-max-time", "30", "--url",
175-
bin.hcfg.credHelperURL, "--output", bin.fullInstallPath())
177+
if !binInstalled {
178+
if bin.helper == "" {
179+
return nil
180+
}
181+
if bin.helper != bin.credHelperConfigName() {
182+
return nil
183+
}
184+
credHelperName := strings.ReplaceAll(bin.credHelperConfigName(), "-login", "")
185+
bin.l.Infof("Installing %s credential helper", credHelperName)
186+
if err := bin.fs.MkdirAll(bin.hcfg.installFolder, 0o700); err != nil {
187+
return fmt.Errorf("error creating installation directory %s, err: %w", bin.hcfg.installFolder, err)
188+
}
176189

177-
_, err = curlCmd.Output()
178-
if err != nil {
179-
return fmt.Errorf("error installing binary %s, err: %w", bin.hcfg.binaryName, err)
190+
curlCmd := bin.cmdCreator.Create(
191+
"curl",
192+
"--retry",
193+
"5",
194+
"--retry-max-time",
195+
"30",
196+
"--url",
197+
bin.hcfg.credHelperURL,
198+
"--output",
199+
bin.fullInstallPath(),
200+
)
201+
202+
if _, err = curlCmd.Output(); err != nil {
203+
return fmt.Errorf("error installing binary %s, err: %w", bin.hcfg.binaryName, err)
204+
}
205+
if err := bin.fs.Chmod(bin.fullInstallPath(), 0o700); err != nil {
206+
return err
207+
}
180208
}
181-
err = bin.fs.Chmod(bin.fullInstallPath(), 0o755)
209+
210+
cfgInstalled, err := bin.configFileInstalled()
182211
if err != nil {
183212
return err
184213
}
185-
err = updateConfigFile(bin)
186-
if err != nil {
187-
return err
214+
215+
if !cfgInstalled {
216+
if err := updateConfigFile(bin); err != nil {
217+
return err
218+
}
188219
}
220+
189221
return nil
190222
}
191223

192224
// RequiresRoot returns whether the installation of the binary needs root permissions.
193225
func (bin *credhelperbin) RequiresRoot() bool {
194226
return false
195227
}
228+
229+
// Using a var function allows overriding during testing.
230+
// This is needed because the curl command directly outputs to a file, but binaryInstalled deletes
231+
// any incorrect file that might exist at the fullInstallPath.
232+
// This means that the mocking method that is typically used to mock filesystem will get the file it
233+
// creates deleted, and then, since cmd.Output() is what is writing the new/correct file, and there's
234+
// no opportunity to mock it or insert the file after it runs, the code that expects the file to exist
235+
// then errors out because it was deleted by binaryInstalled.
236+
var binaryInstalled = func(bin *credhelperbin) (bool, error) {
237+
dirExists, err := afero.DirExists(bin.fs, bin.hcfg.installFolder)
238+
if err != nil {
239+
return false, fmt.Errorf("failed to get status of credential helper directory: %w", err)
240+
}
241+
if !dirExists {
242+
return false, nil
243+
}
244+
fileExists, err := afero.Exists(bin.fs, bin.fullInstallPath())
245+
if err != nil {
246+
return false, fmt.Errorf("failed to get status of credential helper binary: %w", err)
247+
}
248+
if !fileExists {
249+
return false, nil
250+
}
251+
file, err := bin.fs.Open(bin.fullInstallPath())
252+
if err != nil {
253+
return false, fmt.Errorf("failed to open cred helper binary: %w", err)
254+
}
255+
defer file.Close() //nolint:errcheck // closing the file
256+
hash, err := digest.FromReader(file)
257+
if err != nil {
258+
return false, fmt.Errorf("failed to get cred helper binary hash: %w", err)
259+
}
260+
if hash.String() != bin.hcfg.hash {
261+
bin.l.Info("Hash of the installed credential helper binary does not match")
262+
if err := bin.fs.Remove(bin.fullInstallPath()); err != nil {
263+
return false, fmt.Errorf("failed to remove mismatched cred helper binary: %w", err)
264+
}
265+
return false, nil
266+
}
267+
268+
return true, nil
269+
}

Diff for: pkg/dependency/credhelper/cred_helper_binary_test.go

+50-12
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,23 @@ func TestBinaries_Installed(t *testing.T) {
149149
fileData := []byte("")
150150
_, err := mFs.Create("mock_prefix/cred-helpers/docker-credential-binary")
151151
require.NoError(t, err)
152-
err = afero.WriteFile(mFs, "mock_prefix/cred-helpers/docker-credential-binary",
153-
fileData, 0o666)
152+
err = afero.WriteFile(
153+
mFs,
154+
"mock_prefix/cred-helpers/docker-credential-binary",
155+
fileData,
156+
0o666,
157+
)
158+
require.NoError(t, err)
154159

160+
cfgFileData := []byte(`{"credsStore":"binary"}`)
161+
_, err = mFs.Create("mock_prefix/.finch/config.json")
162+
require.NoError(t, err)
163+
err = afero.WriteFile(
164+
mFs,
165+
"mock_prefix/.finch/config.json",
166+
cfgFileData,
167+
0o666,
168+
)
155169
require.NoError(t, err)
156170
},
157171
want: true,
@@ -197,7 +211,8 @@ func TestBinaries_Installed(t *testing.T) {
197211
tc.mockSvc(t, mFs, l)
198212
hc := helperConfig{
199213
"docker-credential-binary", "",
200-
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "mock_prefix/cred-helpers/",
214+
"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
215+
"mock_prefix/cred-helpers/",
201216
"mock_prefix/.finch/",
202217
}
203218
// hash of an empty file
@@ -208,59 +223,82 @@ func TestBinaries_Installed(t *testing.T) {
208223
}
209224
}
210225

226+
//nolint:paralleltest // This function manipulates a global variable to facilitate mocking.
211227
func TestBinaries_Install(t *testing.T) {
212-
t.Parallel()
213-
214228
testCases := []struct {
215229
name string
216230
mockSvc func(
217231
l *mocks.Logger,
218232
cmd *mocks.Command,
219233
creator *mocks.CommandCreator,
220-
mFs afero.Fs)
221-
want error
234+
mFs afero.Fs,
235+
)
236+
want error
237+
postRunCheck func(t *testing.T, mFs afero.Fs)
222238
}{
223239
{
224240
name: "happy path",
225241
mockSvc: func(l *mocks.Logger, cmd *mocks.Command, creator *mocks.CommandCreator, mFs afero.Fs) {
242+
binaryInstalled = func(*credhelperbin) (bool, error) {
243+
return false, nil
244+
}
226245
_, err := mFs.Create("mock_prefix/cred-helpers/docker-credential-ecr-login")
227246
require.NoError(t, err)
228-
cmd.EXPECT().Output().Times(2)
247+
cmd.EXPECT().Output()
229248
l.EXPECT().Infof("Installing %s credential helper", "ecr")
230-
creator.EXPECT().Create("mkdir", "-p", "mock_prefix/cred-helpers/").Return(cmd)
231249
creator.EXPECT().Create("curl", "--retry", "5", "--retry-max-time", "30", "--url",
232250
"https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com"+
233251
"/0.7.0/linux-arm64/docker-credential-ecr-login", "--output",
234252
"mock_prefix/cred-helpers/docker-credential-ecr-login").Return(cmd)
235253
},
236254
want: nil,
255+
postRunCheck: func(t *testing.T, mFs afero.Fs) {
256+
f, err := afero.ReadFile(mFs, "mock_prefix/.finch/config.json")
257+
require.NoError(t, err)
258+
require.Equal(t, string(f), `{"credsStore":"ecr-login"}`)
259+
},
260+
},
261+
{
262+
name: "credential helper already installed, but config file not configured",
263+
mockSvc: func(l *mocks.Logger, cmd *mocks.Command, creator *mocks.CommandCreator, mFs afero.Fs) {
264+
binaryInstalled = func(*credhelperbin) (bool, error) {
265+
return true, nil
266+
}
267+
},
268+
want: nil,
269+
postRunCheck: func(t *testing.T, mFs afero.Fs) {
270+
f, err := afero.ReadFile(mFs, "mock_prefix/.finch/config.json")
271+
require.NoError(t, err)
272+
require.Equal(t, string(f), `{"credsStore":"ecr-login"}`)
273+
},
237274
},
238275
}
239276

240277
for _, tc := range testCases {
241278
tc := tc
242279
t.Run(tc.name, func(t *testing.T) {
243-
t.Parallel()
244-
245280
ctrl := gomock.NewController(t)
246281
cmd := mocks.NewCommand(ctrl)
247282
mFs := afero.NewMemMapFs()
248283
l := mocks.NewLogger(ctrl)
249284
creator := mocks.NewCommandCreator(ctrl)
285+
origBinaryInstalled := binaryInstalled
250286
tc.mockSvc(l, cmd, creator, mFs)
251287

252288
credHelperURL := "https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com" +
253289
"/0.7.0/linux-arm64/docker-credential-ecr-login"
254290

255291
hc := helperConfig{
256292
"docker-credential-ecr-login", credHelperURL,
257-
"sha256:ff14a4da40d28a2d2d81a12a7c9c36294ddf8e6439780c4ccbc96622991f3714",
293+
"sha256:ec5c04babea79b08dffb0c8acb67b9e28dc05be0fe9bd4df2e234d75516061d7",
258294
"mock_prefix/cred-helpers/",
259295
"mock_prefix/.finch/",
260296
}
261297
fc := "ecr-login"
262298
got := newCredHelperBinary(mockFinchPath, mFs, creator, l, fc, "", hc).Install()
299+
binaryInstalled = origBinaryInstalled
263300
assert.Equal(t, tc.want, got)
301+
tc.postRunCheck(t, mFs)
264302
})
265303
}
266304
}

0 commit comments

Comments
 (0)