Skip to content

Commit

Permalink
added credential helper integration
Browse files Browse the repository at this point in the history
Signed-off-by: kiryl1 <[email protected]>
  • Loading branch information
kiryl1 committed Jul 3, 2023
1 parent fef6e77 commit 0d287cc
Show file tree
Hide file tree
Showing 8 changed files with 611 additions and 11 deletions.
7 changes: 6 additions & 1 deletion cmd/finch/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/runfinch/finch/pkg/command"
"github.com/runfinch/finch/pkg/config"
"github.com/runfinch/finch/pkg/dependency"
"github.com/runfinch/finch/pkg/dependency/credhelper"
"github.com/runfinch/finch/pkg/dependency/vmnet"
"github.com/runfinch/finch/pkg/disk"
"github.com/runfinch/finch/pkg/flog"
Expand Down Expand Up @@ -120,7 +121,11 @@ func virtualMachineCommands(
fs afero.Fs,
fc *config.Finch,
) *cobra.Command {
optionalDepGroups := []*dependency.Group{vmnet.NewDependencyGroup(ecc, lcc, fs, fp, logger)}
optionalDepGroups := []*dependency.Group{
vmnet.NewDependencyGroup(ecc, lcc, fs, fp, logger),
credhelper.NewDependencyGroup(ecc, fs, fp, logger, fc, system.NewStdLib().Env("USER"),
system.NewStdLib().Arch()),
}
return newVirtualMachineCommand(
lcc,
logger,
Expand Down
5 changes: 3 additions & 2 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ type AdditionalDirectory struct {

// Finch represents the configuration file for Finch CLI.
type Finch struct {
CPUs *int `yaml:"cpus"`
Memory *string `yaml:"memory"`
CPUs *int `yaml:"cpus"`
Memory *string `yaml:"memory"`
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.
Expand Down
29 changes: 24 additions & 5 deletions pkg/config/nerdctl_config_applier.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ func NewNerdctlApplier(dialer fssh.Dialer, fs afero.Fs, privateKeyPath, 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", profStr, cmd)
if err := afero.WriteFile(fs, profileFilePath, []byte(profBufWithCmd), 0o644); err != nil {
return "", fmt.Errorf("failed to write to profile file: %w", err)
}
return profBufWithCmd, nil
}
return profStr, nil
}

// updateEnvironment adds variables to the user's shell's environment. Currently it uses ~/.bashrc because
// Bash is the default shell and Bash will not load ~/.profile if ~/.bash_profile exists (which it does).
// ~/.bash_profile sources ~/.bashrc, so ~/.bashrc is currently the best place to define additional variables.
Expand All @@ -56,18 +67,26 @@ func NewNerdctlApplier(dialer fssh.Dialer, fs afero.Fs, privateKeyPath, hostUser
// [GNU docs for Bash]: https://www.gnu.org/software/bash/manual/html_node/Bash-Startup-Files.html
//
// [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),
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),
}

profileFilePath := fmt.Sprintf("/home/%s.linux/.bashrc", user)
profBuf, err := afero.ReadFile(fs, profileFilePath)
if err != nil {
return fmt.Errorf("failed to read config file: %w", err)
}

profStr := string(profBuf)
if !strings.Contains(profStr, "export DOCKER_CONFIG") {
profBufWithDockerCfg := fmt.Sprintf("%s\nexport DOCKER_CONFIG=\"/Users/%s/.finch\"\n", profStr, user)
if err := afero.WriteFile(fs, profileFilePath, []byte(profBufWithDockerCfg), 0o644); err != nil {
return fmt.Errorf("failed to write to profile file: %w", err)
for _, element := range cmdArr {
profStr, err = addLinetoFilePath(fs, profileFilePath, profStr, element)
if err != nil {
return err
}
}

Expand Down
18 changes: 15 additions & 3 deletions pkg/config/nerdctl_config_applier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ 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,
},
Expand All @@ -60,15 +65,22 @@ 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,
),
)
},
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,
},
Expand Down
70 changes: 70 additions & 0 deletions pkg/dependency/credhelper/cred_helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package credhelper for integrating credential helpers into Finch
package credhelper

import (
"fmt"

"github.com/spf13/afero"

"github.com/runfinch/finch/pkg/command"
"github.com/runfinch/finch/pkg/config"
"github.com/runfinch/finch/pkg/dependency"
"github.com/runfinch/finch/pkg/flog"
"github.com/runfinch/finch/pkg/path"
)

const (
description = "Installing Credential Helper"
errMsg = "Failed to finish installing credential helper"
)

// NewDependencyGroup returns a dependency group that contains all the dependencies required to make credhelper work.
func NewDependencyGroup(
execCmdCreator command.Creator,
fs afero.Fs,
fp path.Finch,
logger flog.Logger,
fc *config.Finch,
user string,
arch string,
) *dependency.Group {
deps := newDeps(execCmdCreator, fs, fp, logger, fc, user, arch)
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,
fp path.Finch,
logger flog.Logger,
fc *config.Finch,
user string,
arch string,
) []dependency.Dependency {
var deps []dependency.Dependency

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)
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
}
188 changes: 188 additions & 0 deletions pkg/dependency/credhelper/cred_helper_binary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

package credhelper

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/opencontainers/go-digest"
"github.com/spf13/afero"

"github.com/runfinch/finch/pkg/command"
"github.com/runfinch/finch/pkg/config"
"github.com/runfinch/finch/pkg/dependency"
"github.com/runfinch/finch/pkg/flog"
"github.com/runfinch/finch/pkg/path"

"github.com/docker/cli/cli/config/configfile"
)

type binaries struct {
fp path.Finch
fs afero.Fs
cmdCreator command.Creator
l flog.Logger
fc *config.Finch
user 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, hcfg helperConfig,
) *binaries {
return &binaries{
// TODO: consider replacing fp with only the strings that are used instead of the entire type
fp: fp,
fs: fs,
cmdCreator: cmdCreator,
l: l,
fc: fc,
user: user,
hcfg: hcfg,
}
}

func updateConfigFile(bin *binaries) error {
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 {
file, err := bin.fs.Create(cfgPath)
if err != nil {
return err
}
JSONstr := fmt.Sprintf("{\"credsStore\":\"%s\"}", binCfgName)
JSON := []byte(JSONstr)
_, err = file.Write(JSON)
if err != nil {
return err
}
} else {
fileRead, err := bin.fs.Open(cfgPath)
if err != nil {
return err
}
bytes, err := afero.ReadAll(fileRead)
if err != nil {
return err
}
var cfg configfile.ConfigFile
err = json.Unmarshal(bytes, &cfg)
if err != nil {
return err
}
credsStore := cfg.CredentialsStore
defer fileRead.Close() //nolint:errcheck // closing the file
if strings.Compare(credsStore, binCfgName) != 0 {
file, err := bin.fs.OpenFile(cfgPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o755)
if err != nil {
return err
}
cfg.CredentialsStore = binCfgName
finalCfgBytes, err := json.Marshal(&cfg)
if err != nil {
return err
}
_, err = file.Write(finalCfgBytes)
if err != nil {
return err
}
defer file.Close() //nolint:errcheck // closing the file before exit
}
}

return nil
}

func (bin *binaries) credHelperConfigName() string {
return strings.ReplaceAll(bin.hcfg.binaryName, "docker-credential-", "")
}

func (bin *binaries) fullInstallPath() string {
return fmt.Sprintf("%s%s", bin.hcfg.installFolder, bin.hcfg.binaryName)
}

func (bin *binaries) Installed() bool {
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
}
if !dirExists {
return false
}
fileExists, err := afero.Exists(bin.fs, bin.fullInstallPath())
if err != nil {
bin.l.Errorf("failed to get status of credential helper binary: %v", err)
return false
}
if !fileExists {
return false
}
file, err := bin.fs.Open(bin.fullInstallPath())
if err != nil {
bin.l.Error(err)
return false
}
hash, err := digest.FromReader(file)
if err != nil {
bin.l.Error(err)
return false
}
defer file.Close() //nolint:errcheck // closing the file
if strings.Compare(hash.String(), bin.hcfg.hash) != 0 {
bin.l.Info("Hash of the installed credential helper binary does not match")
err := bin.fs.Remove(bin.fullInstallPath())
if err != nil {
bin.l.Error(err)
}
return false
}
return true
}

func (bin *binaries) Install() error {
credsHelper := bin.fc.CredsHelper
if credsHelper == nil {
return nil
}
if strings.Compare(*credsHelper, bin.credHelperConfigName()) != 0 {
return nil
}
bin.l.Info("Installing credential helper")
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.hcfg.installFolder, err)
}

curlCmd := bin.cmdCreator.Create("curl", "--retry", "5", "--retry-max-time", "30", "--url",
bin.hcfg.credHelperURL, "--output", bin.fullInstallPath())

_, err = curlCmd.Output()
if err != nil {
return fmt.Errorf("error installation binary %s, err: %w", bin.hcfg.binaryName, err)
}
err = bin.fs.Chmod(bin.fullInstallPath(), 0o755)
if err != nil {
return err
}
err = updateConfigFile(bin)
if err != nil {
return err
}
return nil
}

func (bin *binaries) RequiresRoot() bool {
return false
}
Loading

0 comments on commit 0d287cc

Please sign in to comment.