diff --git a/internal/configuration/config.go b/internal/configuration/config.go index ab4a970..a64977d 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -9,11 +9,13 @@ import ( "fmt" "io" "os" + "os/exec" "path/filepath" "regexp" "sort" "strings" + "github.com/cloudbees-io/configure-git-global-credentials/internal" "github.com/go-git/go-git/v5/config" format "github.com/go-git/go-git/v5/plumbing/format/config" "github.com/go-git/go-git/v5/plumbing/transport" @@ -46,7 +48,12 @@ type Config struct { GitLabServerURL string `mapstructure:"gitlab-server-url"` } -func loadConfig(scope config.Scope) (_ *format.Config, _ string, retErr error) { +const ( + tokenEnv = "CLOUDBEES_API_TOKEN" + cbGitCredentialsHelperPath = "git-credential-cloudbees" +) + +var loadConfig = func(scope config.Scope) (_ *format.Config, _ string, retErr error) { paths, err := config.Paths(scope) if err != nil { return nil, "", err @@ -153,6 +160,15 @@ func (c *Config) Apply(ctx context.Context) error { return err } + gitCredCloudbeesExists := true + cbGitCredentialsHelperPath, err := exec.LookPath(cbGitCredentialsHelperPath) + if err != nil { + internal.Debug("Could not find git-credential-cloudbees on the path, falling back to old-style helper") + gitCredCloudbeesExists = false + } else { + internal.Debug("Found git-credential-cloudbees on the path at %s", cbGitCredentialsHelperPath) + } + homePath := os.Getenv("HOME") actionPath := filepath.Join(homePath, ".cloudbees-configure-git-global-credentials", c.uniqueId()) if err := os.MkdirAll(actionPath, os.ModePerm); err != nil { @@ -164,35 +180,44 @@ func (c *Config) Apply(ctx context.Context) error { var helperConfigFile string if !c.ssh() { - fmt.Println("🔄 Installing credentials helper ...") + if !gitCredCloudbeesExists { + fmt.Println("🔄 Installing credentials helper ...") - self, err := os.Executable() - if err != nil { - return err - } + self, err := os.Executable() + if err != nil { + return err + } - helperExecutable := filepath.Join(actionPath, "git-credential-helper") - if a, err := filepath.Abs(helperExecutable); err != nil { - helperExecutable = a - } + helperExecutable := filepath.Join(actionPath, "git-credential-helper") + if a, err := filepath.Abs(helperExecutable); err != nil { + helperExecutable = a + } - err = copyFileHelper(helperExecutable, self) - if err != nil { - return err - } + err = copyFileHelper(helperExecutable, self) + if err != nil { + return err + } - fmt.Println("✅ Credentials helper installed") + fmt.Println("✅ Credentials helper installed") - helperConfig = &format.Config{} - helperConfigFile = helperExecutable + ".cfg" - helper = fmt.Sprintf("%s credential-helper --config-file %s", helperExecutable, helperConfigFile) + helperConfig = &format.Config{} + helperConfigFile = helperExecutable + ".cfg" + helper = fmt.Sprintf("%s credential-helper --config-file %s", helperExecutable, helperConfigFile) - if _, err := os.Stat(helperConfigFile); err != nil { - b, err := os.ReadFile(helperConfigFile) - if err == nil { - // make best effort to merge existing, if it fails we will overwrite the whole - _ = format.NewDecoder(bytes.NewReader(b)).Decode(helperConfig) + if _, err := os.Stat(helperConfigFile); err != nil { + b, err := os.ReadFile(helperConfigFile) + if err == nil { + // make best effort to merge existing, if it fails we will overwrite the whole + _ = format.NewDecoder(bytes.NewReader(b)).Decode(helperConfig) + } } + } else { + filterUrl := make([]string, 0, len(aliases)) + for url := range aliases { + filterUrl = append(filterUrl, url) + } + + return invokeGitCredentialsHelper(ctx, cbGitCredentialsHelperPath, cfgPath, c.CloudBeesApiURL, c.CloudBeesApiToken, filterUrl) } } else { // check if the SSH key looks to be a base64 encoded private key that the user forgot to decode @@ -308,6 +333,36 @@ func (c *Config) Apply(ctx context.Context) error { return nil } +var invokeGitCredentialsHelper = func(ctx context.Context, path, gitConfigPath, cloudbeesApiURL, cloudbeesApiToken string, filterGitUrls []string) error { + + homeDir, err := os.UserHomeDir() + if err != nil { + return err + } + helperConfig := filepath.Join(homeDir, ".git-credential-cloudbees-config") + + filterUrlArgs := []string{} + + filterUrlArgs = append(filterUrlArgs, "init") + filterUrlArgs = append(filterUrlArgs, "--config", helperConfig) + filterUrlArgs = append(filterUrlArgs, "--cloudbees-api-token-env-var", tokenEnv) + filterUrlArgs = append(filterUrlArgs, "--cloudbees-api-url", cloudbeesApiURL) + filterUrlArgs = append(filterUrlArgs, "--git-config-file-path", gitConfigPath) + for _, filterGitUrl := range filterGitUrls { + filterUrlArgs = append(filterUrlArgs, "--filter-git-urls", filterGitUrl) + } + cmd := exec.CommandContext(ctx, path, filterUrlArgs...) + + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + internal.Debug("%s", cmd.String()) + + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", tokenEnv, cloudbeesApiToken)) + + return cmd.Run() +} + func (c *Config) providerUsername() string { switch c.Provider { case "github": diff --git a/internal/configuration/config_test.go b/internal/configuration/config_test.go index 75604a3..dfe7e6b 100644 --- a/internal/configuration/config_test.go +++ b/internal/configuration/config_test.go @@ -1,8 +1,15 @@ package configuration import ( + "bytes" + "context" + "os" + "path/filepath" "testing" + "github.com/go-git/go-git/v5/config" + format "github.com/go-git/go-git/v5/plumbing/format/config" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -603,3 +610,99 @@ func TestConfig_insteadOfURLs(t *testing.T) { }) } } + +func TestConfig_Apply_Scenarios(t *testing.T) { + originalCfg, _, err := loadConfig(config.GlobalScope) + require.NoError(t, err) + tests := []struct { + name string + config Config + setupCredentialsHelper bool + }{ + { + name: "with credentials helper", + config: Config{ + Repositories: "user/repo", + Provider: "github", + }, + setupCredentialsHelper: true, + }, + { + name: "without credentials helper", + config: Config{ + Repositories: "user/repo", + Provider: "github", + }, + setupCredentialsHelper: false, + }, + { + name: "with credentials helper and ssh", + config: Config{ + Repositories: "user/repo", + Provider: "github", + SshKey: `-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACB5tesp0633JJ+Q2hfpUXljwtBX263Tq9ENr76NdZ9e3wAAAKAFw5AuBcOQ +LgAAAAtzc2gtZWQyNTUxOQAAACB5tesp0633JJ+Q2hfpUXljwtBX263Tq9ENr76NdZ9e3w +AAAEApe1n3xwD4plUvs5E82QSBggtUz1M6HiiaVEYWp7ybpnm16ynTrfckn5DaF+lReWPC +0FfbrdOr0Q2vvo11n17fAAAAFnlvdXJfZW1haWxAZXhhbXBsZS5jb20BAgMEBQYH +-----END OPENSSH PRIVATE KEY----- +`, + }, + setupCredentialsHelper: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Reset the credentials helper invocation flag + gitCredentialsHelperInvoked := false + + // mock credentials helper function + invokeGitCredentialsHelper = func(ctx context.Context, path, gitConfigPath, cloudbeesApiURL, cloudbeesApiToken string, filterGitUrls []string) error { + gitCredentialsHelperInvoked = true + return nil + } + + // mock loadConfig helper function + loadConfig = func(scope config.Scope) (_ *format.Config, _ string, retErr error) { + + tempGitConfig := filepath.Join(t.TempDir(), ".gitconfig") + + var b bytes.Buffer + err = format.NewEncoder(&b).Encode(originalCfg) + require.NoError(t, err) + + err = os.WriteFile(tempGitConfig, b.Bytes(), 0666) + require.NoError(t, err) + + return originalCfg, tempGitConfig, nil + } + + if tt.setupCredentialsHelper { + helperBinary(t) + } + + // Execute the Apply method + err := tt.config.Apply(context.Background()) + assert.NoError(t, err) + + if tt.config.ssh() { + assert.Equal(t, false, gitCredentialsHelperInvoked) + } else { + assert.Equal(t, tt.setupCredentialsHelper, gitCredentialsHelperInvoked) + } + + }) + } +} + +func helperBinary(t *testing.T) { + binPath := filepath.Join(t.TempDir(), cbGitCredentialsHelperPath) + err := os.WriteFile(binPath, []byte("dummy git credential helper"), 0500) + require.NoError(t, err) + + err = os.Setenv("PATH", filepath.Dir(binPath)) + require.NoError(t, err) + +} diff --git a/internal/util.go b/internal/util.go new file mode 100644 index 0000000..83e7e4b --- /dev/null +++ b/internal/util.go @@ -0,0 +1,12 @@ +package internal + +import ( + "fmt" + "os" +) + +func Debug(msg string, args ...any) { + if os.Getenv("RUNNER_DEBUG") == "1" { + fmt.Println("##[debug]" + fmt.Sprintf(msg, args...)) + } +}