diff --git a/pkg/config/config.go b/pkg/config/config.go index 144f4df7b..0360dc042 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -35,7 +35,7 @@ type AdditionalDirectory struct { type Finch struct { CPUs *int `yaml:"cpus"` Memory *string `yaml:"memory"` - CredsHelper *string `yaml:"credsHelper"` + CredsHelper *string `yaml:"credsHelper,omitempty"` // AdditionalDirectories are the work directories that are not supported by default. In macOS, only home directory is supported by default. // For example, if you want to mount a directory into a container, and that directory is not under your home directory, // then you'll need to specify this field to add that directory or any ascendant of it as a work directory. diff --git a/pkg/config/nerdctl_config_applier.go b/pkg/config/nerdctl_config_applier.go index 38d9b8898..1c14e29bb 100644 --- a/pkg/config/nerdctl_config_applier.go +++ b/pkg/config/nerdctl_config_applier.go @@ -41,9 +41,10 @@ func NewNerdctlApplier(dialer fssh.Dialer, fs afero.Fs, privateKeyPath, hostUser hostUser: hostUser, } } + func addLinetoFilePath(fs afero.Fs, profileFilePath string, profStr string, cmd string) (string, error) { if !strings.Contains(profStr, cmd) { - profBufWithCmd := fmt.Sprintf("%s\n%s\n", profStr, cmd) + profBufWithCmd := fmt.Sprintf("%s\n%s", profStr, cmd) if err := afero.WriteFile(fs, profileFilePath, []byte(profBufWithCmd), 0o644); err != nil { return "", fmt.Errorf("failed to write to profile file: %w", err) } @@ -68,11 +69,13 @@ func addLinetoFilePath(fs afero.Fs, profileFilePath string, profStr string, cmd // [registry nerdctl docs]: https://github.com/containerd/nerdctl/blob/master/docs/registry.md func updateEnvironment(fs afero.Fs, user string) error { - cmdArr := [4]string{fmt.Sprintf("export DOCKER_CONFIG=\"/Users/%s/.finch\"", user), + cmdArr := [4]string{ + fmt.Sprintf("export DOCKER_CONFIG=\"/Users/%s/.finch\"", user), fmt.Sprintf("[ -L /usr/local/bin/docker-credential-ecr-login ] "+ "|| sudo ln -s /Users/%s/.finch/cred-helpers/docker-credential-ecr-login /usr/local/bin/", user), fmt.Sprintf("[ -L $HOME/.aws ] || ln -s /Users/%s/.aws $HOME/.aws", user), - fmt.Sprintf("[ -L /root/.aws ] || sudo ln -fs /Users/%s/.aws /root/.aws", user)} + fmt.Sprintf("[ -L /root/.aws ] || sudo ln -fs /Users/%s/.aws /root/.aws", user), + } profileFilePath := fmt.Sprintf("/home/%s.linux/.bashrc", user) profBuf, err := afero.ReadFile(fs, profileFilePath) diff --git a/pkg/config/nerdctl_config_applier_test.go b/pkg/config/nerdctl_config_applier_test.go index 512a9395c..01090533e 100644 --- a/pkg/config/nerdctl_config_applier_test.go +++ b/pkg/config/nerdctl_config_applier_test.go @@ -47,7 +47,11 @@ func Test_updateEnvironment(t *testing.T) { postRunCheck: func(t *testing.T, fs afero.Fs) { fileBytes, err := afero.ReadFile(fs, "/home/mock_user.linux/.bashrc") require.NoError(t, err) - assert.Equal(t, []byte("\n"+`export DOCKER_CONFIG="/Users/mock_user/.finch"`+"\n"), fileBytes) + assert.Equal(t, + []byte("\nexport DOCKER_CONFIG=\"/Users/mock_user/.finch\"\n[ -L /usr/local/bin/docker-credential-ecr-login ] || sudo ln -s "+ + "/Users/mock_user/.finch/cred-helpers/docker-credential-ecr-login /usr/local/bin/"+ + "\n[ -L $HOME/.aws ] || ln -s /Users/mock_user/.aws $HOME/.aws\n"+ + "[ -L /root/.aws ] || sudo ln -fs /Users/mock_user/.aws /root/.aws"), fileBytes) }, want: nil, }, @@ -60,7 +64,10 @@ func Test_updateEnvironment(t *testing.T) { afero.WriteFile( fs, "/home/mock_user.linux/.bashrc", - []byte(`export DOCKER_CONFIG="/Users/mock_user/.finch"`), + []byte("export DOCKER_CONFIG=\"/Users/mock_user/.finch\""+"\n"+"[ -L /usr/local/bin/docker-credential-ecr-login ] "+ + "|| sudo ln -s /Users/mock_user/.finch/cred-helpers/docker-credential-ecr-login /usr/local/bin/"+ + "\n"+"[ -L $HOME/.aws ] || ln -s /Users/mock_user/.aws $HOME/.aws"+"\n"+ + "[ -L /root/.aws ] || sudo ln -fs /Users/mock_user/.aws /root/.aws"), 0o644, ), ) @@ -68,7 +75,10 @@ func Test_updateEnvironment(t *testing.T) { postRunCheck: func(t *testing.T, fs afero.Fs) { fileBytes, err := afero.ReadFile(fs, "/home/mock_user.linux/.bashrc") require.NoError(t, err) - assert.Equal(t, []byte(`export DOCKER_CONFIG="/Users/mock_user/.finch"`), fileBytes) + assert.Equal(t, []byte(`export DOCKER_CONFIG="/Users/mock_user/.finch"`+"\n"+"[ -L /usr/local/bin/docker-credential-ecr-login ] "+ + "|| sudo ln -s /Users/mock_user/.finch/cred-helpers/docker-credential-ecr-login /usr/local/bin/"+ + "\n"+"[ -L $HOME/.aws ] || ln -s /Users/mock_user/.aws $HOME/.aws"+"\n"+ + "[ -L /root/.aws ] || sudo ln -fs /Users/mock_user/.aws /root/.aws"), fileBytes) }, want: nil, }, diff --git a/pkg/dependency/credHelper/cred_helper.go b/pkg/dependency/credHelper/cred_helper.go index 097d26256..8413cdd27 100644 --- a/pkg/dependency/credHelper/cred_helper.go +++ b/pkg/dependency/credHelper/cred_helper.go @@ -33,6 +33,14 @@ func NewDependencyGroup( return dependency.NewGroup(deps, description, errMsg) } +type helperConfig struct { + binaryName string + credHelperUrl string + hash string + installFolder string + finchPath string +} + func newDeps( execCmdCreator command.Creator, fs afero.Fs, @@ -46,8 +54,14 @@ func newDeps( credHelperUrl := fmt.Sprintf("https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com"+ "/0.7.0/linux-%s/docker-credential-ecr-login", arch) - binaries := newCredHelperBinary(fp, fs, execCmdCreator, logger, fc, user, "docker-credential-ecr-login", - credHelperUrl, "sha256:ff14a4da40d28a2d2d81a12a7c9c36294ddf8e6439780c4ccbc96622991f3714") + installFolder := fmt.Sprintf("/Users/%s/.finch/cred-helpers/", user) + finchPath := fmt.Sprintf("/Users/%s/.finch/", user) + hc := helperConfig{ + binaryName: "docker-credential-ecr-login", credHelperUrl: credHelperUrl, + hash: "sha256:ff14a4da40d28a2d2d81a12a7c9c36294ddf8e6439780c4ccbc96622991f3714", installFolder: installFolder, + finchPath: finchPath, + } + binaries := newCredHelperBinary(fp, fs, execCmdCreator, logger, fc, user, hc) deps = append(deps, dependency.Dependency(binaries)) return deps diff --git a/pkg/dependency/credHelper/cred_helper_binary.go b/pkg/dependency/credHelper/cred_helper_binary.go index ac44db783..f0c2456f6 100644 --- a/pkg/dependency/credHelper/cred_helper_binary.go +++ b/pkg/dependency/credHelper/cred_helper_binary.go @@ -53,15 +53,14 @@ type binaries struct { l flog.Logger fc *config.Finch user string - binaryName string - binaryUrl string - hash string + hcfg helperConfig } var _ dependency.Dependency = (*binaries)(nil) func newCredHelperBinary(fp path.Finch, fs afero.Fs, cmdCreator command.Creator, l flog.Logger, fc *config.Finch, - user string, binaryName string, binaryUrl string, hash string) *binaries { + user string, hcfg helperConfig, +) *binaries { return &binaries{ // TODO: consider replacing fp with only the strings that are used instead of the entire type fp: fp, @@ -70,21 +69,19 @@ func newCredHelperBinary(fp path.Finch, fs afero.Fs, cmdCreator command.Creator, l: l, fc: fc, user: user, - binaryName: binaryName, - binaryUrl: binaryUrl, - hash: hash, + hcfg: hcfg, } } func updateConfigFile(bin *binaries) error { - cfgPath := fmt.Sprintf("%s%s", bin.finchPath(), "config.json") + cfgPath := fmt.Sprintf("%s%s", bin.hcfg.finchPath, "config.json") binCfgName := bin.credHelperConfigName() fileExists, err := afero.Exists(bin.fs, cfgPath) if err != nil { return err } if !fileExists { - //create file + file, err := bin.fs.Create(cfgPath) if err != nil { return err @@ -109,7 +106,7 @@ func updateConfigFile(bin *binaries) error { credsStore := cfg.CredentialsStore defer fileRead.Close() if strings.Compare(credsStore, binCfgName) != 0 { - //if credsStore field does not match finch.yaml + //populate credsStore field with correct name file, err := bin.fs.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755) if err != nil { @@ -132,23 +129,15 @@ func updateConfigFile(bin *binaries) error { } func (bin *binaries) credHelperConfigName() string { - return strings.Replace(bin.binaryName, "docker-credential-", "", -1) -} - -func (bin *binaries) installationPath() string { - return fmt.Sprintf("/Users/%s/.finch/cred-helpers/", bin.user) -} - -func (bin *binaries) finchPath() string { - return fmt.Sprintf("/Users/%s/.finch/", bin.user) + return strings.Replace(bin.hcfg.binaryName, "docker-credential-", "", -1) } func (bin *binaries) fullInstallPath() string { - return fmt.Sprintf("%s%s", bin.installationPath(), bin.binaryName) + return fmt.Sprintf("%s%s", bin.hcfg.installFolder, bin.hcfg.binaryName) } func (bin *binaries) Installed() bool { - dirExists, err := afero.DirExists(bin.fs, bin.installationPath()) + dirExists, err := afero.DirExists(bin.fs, bin.hcfg.installFolder) if err != nil { bin.l.Errorf("failed to get status of binaries directory: %v", err) return false @@ -175,7 +164,7 @@ func (bin *binaries) Installed() bool { return false } defer file.Close() - if strings.Compare(hash.String(), bin.hash) != 0 { + if strings.Compare(hash.String(), bin.hcfg.hash) != 0 { bin.l.Error("Hash of the installed credential helper binary does not match") err := bin.fs.Remove(bin.fullInstallPath()) if err != nil { @@ -195,18 +184,18 @@ func (bin *binaries) Install() error { return nil } bin.l.Info("Installing credential helper") - mkdirCmd := bin.cmdCreator.Create("mkdir", "-p", bin.installationPath()) + mkdirCmd := bin.cmdCreator.Create("mkdir", "-p", bin.hcfg.installFolder) _, err := mkdirCmd.Output() if err != nil { - return fmt.Errorf("error creating installation directory %s, err: %w", bin.installationPath(), err) + return fmt.Errorf("error creating installation directory %s, err: %w", bin.hcfg.installFolder, err) } curlCmd := bin.cmdCreator.Create("curl", "--retry", "5", "--retry-max-time", "30", "--url", - bin.binaryUrl, "--output", bin.fullInstallPath()) + bin.hcfg.credHelperUrl, "--output", bin.fullInstallPath()) _, err = curlCmd.Output() if err != nil { - return fmt.Errorf("error installation binary %s, err: %w", bin.binaryName, err) + return fmt.Errorf("error installation binary %s, err: %w", bin.hcfg.binaryName, err) } err = bin.fs.Chmod(bin.fullInstallPath(), 0o755) if err != nil { diff --git a/pkg/dependency/credHelper/cred_helper_binary_test.go b/pkg/dependency/credHelper/cred_helper_binary_test.go new file mode 100644 index 000000000..2c4d27161 --- /dev/null +++ b/pkg/dependency/credHelper/cred_helper_binary_test.go @@ -0,0 +1,153 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Ensures that the binaries required for networking are installed in a privileged location. +// More information here: https://github.com/lima-vm/socket_vmnet +package credHelper + +import ( + "github.com/runfinch/finch/pkg/config" + "io/fs" + "testing" + + "github.com/runfinch/finch/pkg/mocks" + "github.com/runfinch/finch/pkg/path" + + "github.com/golang/mock/gomock" + "github.com/spf13/afero" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + mockFinchPathString = "mock_prefix" + mockFinchPath = path.Finch(mockFinchPathString) +) + +func Test_credHelperConfigName(t *testing.T) { + t.Parallel() + + got := newCredHelperBinary("", nil, nil, nil, nil, "user", + helperConfig{"docker-credential-cred-helper", "", "", + "", ""}).credHelperConfigName() + assert.Equal(t, "cred-helper", got) +} +func Test_fullInstallPath(t *testing.T) { + t.Parallel() + + got := newCredHelperBinary("", nil, nil, nil, nil, "user", + helperConfig{"docker-credential-cred-helper", "", "", "/folder/", + ""}).fullInstallPath() + assert.Equal(t, "/folder/docker-credential-cred-helper", got) +} + +func TestBinaries_Installed(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func(t *testing.T, mFs afero.Fs, l *mocks.Logger) + want bool + }{ + { + name: "happy path", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + require.NoError(t, mFs.MkdirAll("/mock_prefix/cred-helpers/", fs.ModeDir)) + fileData := []byte("") + _, err := mFs.Create("mock_prefix/cred-helpers/docker-credential-binary") + require.NoError(t, err) + err = afero.WriteFile(mFs, "mock_prefix/cred-helpers/docker-credential-binary", + fileData, 0o666) + + require.NoError(t, err) + }, + want: true, + }, + { + name: "installation path doesn't exist", + mockSvc: func(t *testing.T, mFs afero.Fs, l *mocks.Logger) { + }, + want: false, + }} + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + mFs := afero.NewMemMapFs() + l := mocks.NewLogger(ctrl) + tc.mockSvc(t, mFs, l) + hc := helperConfig{"docker-credential-binary", "", + "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "mock_prefix/cred-helpers/", + "mock_prefix/.finch/"} + //hash of an empty file + got := newCredHelperBinary(mockFinchPath, mFs, nil, l, nil, "", hc).Installed() + + assert.Equal(t, tc.want, got) + }) + } +} + +func TestBinaries_Install(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mockSvc func( + l *mocks.Logger, + cmd *mocks.Command, + creator *mocks.CommandCreator, + mFs afero.Fs) + want error + }{ + { + name: "happy path", + mockSvc: func(l *mocks.Logger, cmd *mocks.Command, creator *mocks.CommandCreator, mFs afero.Fs) { + _, err := mFs.Create("mock_prefix/cred-helpers/docker-credential-ecr-login") + require.NoError(t, err) + cmd.EXPECT().Output().Times(2) + l.EXPECT().Info("Installing credential helper") + creator.EXPECT().Create("mkdir", "-p", "mock_prefix/cred-helpers/").Return(cmd) + creator.EXPECT().Create("curl", "--retry", "5", "--retry-max-time", "30", "--url", + "https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com"+ + "/0.7.0/linux-arm64/docker-credential-ecr-login", "--output", "mock_prefix/cred-helpers/docker-credential-ecr-login").Return(cmd) + }, + want: nil, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + cmd := mocks.NewCommand(ctrl) + mFs := afero.NewMemMapFs() + l := mocks.NewLogger(ctrl) + creator := mocks.NewCommandCreator(ctrl) + tc.mockSvc(l, cmd, creator, mFs) + + credHelperUrl := "https://amazon-ecr-credential-helper-releases.s3.us-east-2.amazonaws.com" + + "/0.7.0/linux-arm64/docker-credential-ecr-login" + + hc := helperConfig{"docker-credential-ecr-login", credHelperUrl, + "sha256:ff14a4da40d28a2d2d81a12a7c9c36294ddf8e6439780c4ccbc96622991f3714", + "mock_prefix/cred-helpers/", + "mock_prefix/.finch/"} + fc := "ecr-login" + got := newCredHelperBinary(mockFinchPath, mFs, creator, l, &config.Finch{CredsHelper: &fc}, "", hc).Install() + assert.Equal(t, tc.want, got) + }) + } +} + +func TestBinaries_RequiresRoot(t *testing.T) { + t.Parallel() + + got := newCredHelperBinary(mockFinchPath, nil, nil, nil, nil, "", + helperConfig{}).RequiresRoot() + assert.Equal(t, false, got) +} diff --git a/pkg/dependency/credHelper/cred_helper_test.go b/pkg/dependency/credHelper/cred_helper_test.go new file mode 100644 index 000000000..c5613fba9 --- /dev/null +++ b/pkg/dependency/credHelper/cred_helper_test.go @@ -0,0 +1,29 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package credHelper + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/runfinch/finch/pkg/dependency" +) + +func Test_NewDependencyGroup(t *testing.T) { + t.Parallel() + + want := dependency.NewGroup(newDeps(nil, nil, "", nil, nil, "", ""), description, errMsg) + got := NewDependencyGroup(nil, nil, "", nil, nil, "", "") + assert.Equal(t, want, got) +} + +func Test_newDeps(t *testing.T) { + t.Parallel() + + got := newDeps(nil, nil, "", nil, nil, "", "") + require.Equal(t, 1, len(got)) + assert.IsType(t, (*binaries)(nil), got[0]) +}