mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-08-05 02:38:33 +00:00
feat: Rotate Github App Token outside of Atlantis commands (#3208)
Co-authored-by: nitrocode <7775707+nitrocode@users.noreply.github.com> Co-authored-by: PePe Amengual <jose.amengual@gmail.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -17,6 +17,7 @@ atlantis
|
||||
atlantis.env
|
||||
*.act
|
||||
package-lock.json
|
||||
Dockerfile.local
|
||||
|
||||
# gitreleaser
|
||||
dist/
|
||||
|
||||
@@ -7,9 +7,11 @@ services:
|
||||
- 4040:4040
|
||||
environment:
|
||||
# https://dashboard.ngrok.com/get-started/your-authtoken
|
||||
NGROK_AUTH: REPLACE-WITH-YOUR-TOKEN
|
||||
# NGROK_AUTH: REPLACE-WITH-YOUR-TOKEN // set this in atlantis.env
|
||||
NGROK_PROTOCOL: http
|
||||
NGROK_PORT: atlantis:4141
|
||||
env_file:
|
||||
- ./atlantis.env
|
||||
depends_on:
|
||||
- atlantis
|
||||
redis:
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
)
|
||||
|
||||
func TestApplyCommandRunner_IsLocked(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
@@ -78,6 +79,7 @@ func TestApplyCommandRunner_IsLocked(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestApplyCommandRunner_IsSilenced(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
)
|
||||
|
||||
func TestApproveCommandRunner_IsOwner(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
|
||||
@@ -113,7 +113,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
|
||||
githubGetter = mocks.NewMockGithubPullGetter()
|
||||
gitlabGetter = mocks.NewMockGitlabMergeRequestGetter()
|
||||
azuredevopsGetter = mocks.NewMockAzureDevopsPullGetter()
|
||||
logger = logging.NewNoopLogger(t)
|
||||
logger := logging.NewNoopLogger(t)
|
||||
projectCommandRunner = mocks.NewMockProjectCommandRunner()
|
||||
workingDir = mocks.NewMockWorkingDir()
|
||||
pendingPlanFinder = mocks.NewMockPendingPlanFinder()
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/go-homedir"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
)
|
||||
|
||||
const redactedReplacement = "://:<redacted>@"
|
||||
|
||||
// GithubAppWorkingDir implements WorkingDir.
|
||||
// It acts as a proxy to an instance of WorkingDir that refreshes the app's token
|
||||
// before every clone, given Github App tokens expire quickly
|
||||
@@ -22,34 +21,20 @@ type GithubAppWorkingDir struct {
|
||||
|
||||
// Clone writes a fresh token for Github App authentication
|
||||
func (g *GithubAppWorkingDir) Clone(log logging.SimpleLogging, headRepo models.Repo, p models.PullRequest, workspace string) (string, bool, error) {
|
||||
|
||||
log.Info("Refreshing git tokens for Github App")
|
||||
|
||||
token, err := g.Credentials.GetToken()
|
||||
if err != nil {
|
||||
return "", false, errors.Wrap(err, "getting github token")
|
||||
}
|
||||
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return "", false, errors.Wrap(err, "getting home dir to write ~/.git-credentials file")
|
||||
}
|
||||
|
||||
// https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation
|
||||
if err := WriteGitCreds("x-access-token", token, g.GithubHostname, home, log, true); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
baseRepo := &p.BaseRepo
|
||||
|
||||
// Realistically, this is a super brittle way of supporting clones using gh app installation tokens
|
||||
// This URL should be built during Repo creation and the struct should be immutable going forward.
|
||||
// Doing this requires a larger refactor however, and can probably be coupled with supporting > 1 installation
|
||||
authURL := fmt.Sprintf("://x-access-token:%s", token)
|
||||
baseRepo.CloneURL = strings.Replace(baseRepo.CloneURL, "://:", authURL, 1)
|
||||
baseRepo.SanitizedCloneURL = strings.Replace(baseRepo.SanitizedCloneURL, "://:", "://x-access-token:", 1)
|
||||
headRepo.CloneURL = strings.Replace(headRepo.CloneURL, "://:", authURL, 1)
|
||||
headRepo.SanitizedCloneURL = strings.Replace(baseRepo.SanitizedCloneURL, "://:", "://x-access-token:", 1)
|
||||
|
||||
// This removes the credential part from the url and leaves us with the raw http url
|
||||
// git will then pick up credentials from the credential store which is set in vcs.WriteGitCreds.
|
||||
// Git credentials will then be rotated by vcs.GitCredsTokenRotator
|
||||
replacement := "://"
|
||||
baseRepo.CloneURL = strings.Replace(baseRepo.CloneURL, "://:@", replacement, 1)
|
||||
baseRepo.SanitizedCloneURL = strings.Replace(baseRepo.SanitizedCloneURL, redactedReplacement, replacement, 1)
|
||||
headRepo.CloneURL = strings.Replace(headRepo.CloneURL, "://:@", replacement, 1)
|
||||
headRepo.SanitizedCloneURL = strings.Replace(baseRepo.SanitizedCloneURL, redactedReplacement, replacement, 1)
|
||||
|
||||
return g.WorkingDir.Clone(log, headRepo, p, workspace)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"testing"
|
||||
|
||||
. "github.com/petergtz/pegomock"
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
eventMocks "github.com/runatlantis/atlantis/server/events/mocks"
|
||||
"github.com/runatlantis/atlantis/server/events/models"
|
||||
@@ -57,6 +58,8 @@ func TestClone_GithubAppNoneExisting(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestClone_GithubAppSetsCorrectUrl(t *testing.T) {
|
||||
pegomock.RegisterMockTestingT(t)
|
||||
|
||||
workingDir := eventMocks.NewMockWorkingDir()
|
||||
|
||||
credentials := vcsMocks.NewMockGithubCredentials()
|
||||
@@ -82,8 +85,9 @@ func TestClone_GithubAppSetsCorrectUrl(t *testing.T) {
|
||||
headRepo := baseRepo
|
||||
|
||||
modifiedBaseRepo := baseRepo
|
||||
modifiedBaseRepo.CloneURL = "https://x-access-token:token@github.com/runatlantis/atlantis.git"
|
||||
modifiedBaseRepo.SanitizedCloneURL = "https://x-access-token:<redacted>@github.com/runatlantis/atlantis.git"
|
||||
// remove credentials from both urls since we want to use the credential store
|
||||
modifiedBaseRepo.CloneURL = "https://github.com/runatlantis/atlantis.git"
|
||||
modifiedBaseRepo.SanitizedCloneURL = "https://github.com/runatlantis/atlantis.git"
|
||||
|
||||
When(credentials.GetToken()).ThenReturn("token", nil)
|
||||
When(workingDir.Clone(logger, modifiedBaseRepo, models.PullRequest{BaseRepo: modifiedBaseRepo}, "default")).ThenReturn(
|
||||
@@ -92,5 +96,7 @@ func TestClone_GithubAppSetsCorrectUrl(t *testing.T) {
|
||||
|
||||
_, success, _ := ghAppWorkingDir.Clone(logger, headRepo, models.PullRequest{BaseRepo: baseRepo}, "default")
|
||||
|
||||
workingDir.VerifyWasCalledOnce().Clone(logger, modifiedBaseRepo, models.PullRequest{BaseRepo: modifiedBaseRepo}, "default")
|
||||
|
||||
Assert(t, success == true, "clone url mutation error")
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
)
|
||||
|
||||
func TestImportCommandRunner_Run(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
tests := []struct {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPlanCommandRunner_IsSilenced(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
cases := []struct {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/core/db"
|
||||
"github.com/runatlantis/atlantis/server/jobs"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
|
||||
@@ -187,6 +188,7 @@ func TestCleanUpPullComments(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCleanUpLogStreaming(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
RegisterMockTestingT(t)
|
||||
|
||||
t.Run("Should Clean Up Log Streaming Resources When PR is closed", func(t *testing.T) {
|
||||
|
||||
71
server/events/vcs/gh_app_creds_rotator.go
Normal file
71
server/events/vcs/gh_app_creds_rotator.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
"github.com/runatlantis/atlantis/server/scheduled"
|
||||
)
|
||||
|
||||
// GitCredsTokenRotator continuously tries to rotate the github app access token every 30 seconds and writes the ~/.git-credentials file
|
||||
type GitCredsTokenRotator interface {
|
||||
Run()
|
||||
GenerateJob() (scheduled.JobDefinition, error)
|
||||
}
|
||||
|
||||
type githubAppTokenRotator struct {
|
||||
log logging.SimpleLogging
|
||||
githubCredentials GithubCredentials
|
||||
githubHostname string
|
||||
homeDirPath string
|
||||
}
|
||||
|
||||
func NewGithubAppTokenRotator(
|
||||
log logging.SimpleLogging,
|
||||
githubCredentials GithubCredentials,
|
||||
githubHostname string,
|
||||
homeDirPath string) GitCredsTokenRotator {
|
||||
|
||||
return &githubAppTokenRotator{
|
||||
log: log,
|
||||
githubCredentials: githubCredentials,
|
||||
githubHostname: githubHostname,
|
||||
homeDirPath: homeDirPath,
|
||||
}
|
||||
}
|
||||
|
||||
// make sure interface is implemented correctly
|
||||
var _ GitCredsTokenRotator = (*githubAppTokenRotator)(nil)
|
||||
|
||||
func (r *githubAppTokenRotator) GenerateJob() (scheduled.JobDefinition, error) {
|
||||
|
||||
return scheduled.JobDefinition{
|
||||
Job: r,
|
||||
Period: 30 * time.Second,
|
||||
}, r.rotate()
|
||||
}
|
||||
|
||||
func (r *githubAppTokenRotator) Run() {
|
||||
err := r.rotate()
|
||||
if err != nil {
|
||||
// at least log the error message here, as we want to notify the that user that the key rotation wasn't successful
|
||||
r.log.Err(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (r *githubAppTokenRotator) rotate() error {
|
||||
r.log.Debug("Refreshing git tokens for Github App")
|
||||
|
||||
token, err := r.githubCredentials.GetToken()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "Getting github token")
|
||||
}
|
||||
r.log.Debug("token %s", token)
|
||||
|
||||
// https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation
|
||||
if err := WriteGitCreds("x-access-token", token, r.githubHostname, r.homeDirPath, r.log, true); err != nil {
|
||||
return errors.Wrap(err, "Writing ~/.git-credentials file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
84
server/events/vcs/gh_app_creds_rotator_test.go
Normal file
84
server/events/vcs/gh_app_creds_rotator_test.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package vcs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs/testdata"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
func Test_githubAppTokenRotator_GenerateJob(t *testing.T) {
|
||||
defer disableSSLVerification()()
|
||||
testServer, err := testdata.GithubAppTestServer(t)
|
||||
Ok(t, err)
|
||||
|
||||
anonCreds := &vcs.GithubAnonymousCredentials{}
|
||||
anonClient, err := vcs.NewGithubClient(testServer, anonCreds, vcs.GithubConfig{}, logging.NewNoopLogger(t))
|
||||
Ok(t, err)
|
||||
tempSecrets, err := anonClient.ExchangeCode("good-code")
|
||||
Ok(t, err)
|
||||
type fields struct {
|
||||
githubCredentials vcs.GithubCredentials
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
credsFileWritten bool
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Should write .git-credentials file on start",
|
||||
fields: fields{&vcs.GithubAppCredentials{
|
||||
AppID: tempSecrets.ID,
|
||||
Key: []byte(testdata.GithubPrivateKey),
|
||||
Hostname: testServer,
|
||||
}},
|
||||
credsFileWritten: true,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Should return an error if pem data is missing or wrong",
|
||||
fields: fields{&vcs.GithubAppCredentials{
|
||||
AppID: tempSecrets.ID,
|
||||
Key: []byte("some bad formatted pem key"),
|
||||
Hostname: testServer,
|
||||
}},
|
||||
credsFileWritten: false,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Should return an error if app id is missing or wrong",
|
||||
fields: fields{&vcs.GithubAppCredentials{
|
||||
AppID: 3819,
|
||||
Key: []byte(testdata.GithubPrivateKey),
|
||||
Hostname: testServer,
|
||||
}},
|
||||
credsFileWritten: false,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
r := vcs.NewGithubAppTokenRotator(logging.NewNoopLogger(t), tt.fields.githubCredentials, testServer, tmpDir)
|
||||
got, err := r.GenerateJob()
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("githubAppTokenRotator.GenerateJob() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.credsFileWritten {
|
||||
credsFileContent := fmt.Sprintf(`https://x-access-token:some-token@%s`, testServer)
|
||||
actContents, err := os.ReadFile(filepath.Join(tmpDir, ".git-credentials"))
|
||||
Ok(t, err)
|
||||
Equals(t, credsFileContent, string(actContents))
|
||||
}
|
||||
Equals(t, 30*time.Second, got.Period)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package events
|
||||
package vcs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package events_test
|
||||
package vcs_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -7,18 +7,17 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/runatlantis/atlantis/server/events"
|
||||
"github.com/runatlantis/atlantis/server/events/vcs"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
. "github.com/runatlantis/atlantis/testing"
|
||||
)
|
||||
|
||||
var logger logging.SimpleLogging
|
||||
|
||||
// Test that we write the file as expected
|
||||
func TestWriteGitCreds_WriteFile(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
err := events.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
err := vcs.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
Ok(t, err)
|
||||
|
||||
expContents := `https://user:token@hostname`
|
||||
@@ -31,13 +30,14 @@ func TestWriteGitCreds_WriteFile(t *testing.T) {
|
||||
// Test that if the file already exists and it doesn't have the line we would
|
||||
// have written, we write it.
|
||||
func TestWriteGitCreds_Appends(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
err := os.WriteFile(credsFile, []byte("contents"), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
err = events.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
err = vcs.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
Ok(t, err)
|
||||
|
||||
expContents := "contents\nhttps://user:token@hostname"
|
||||
@@ -49,6 +49,7 @@ func TestWriteGitCreds_Appends(t *testing.T) {
|
||||
// Test that if the file already exists and it already has the line expected
|
||||
// we do nothing.
|
||||
func TestWriteGitCreds_NoModification(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
@@ -56,7 +57,7 @@ func TestWriteGitCreds_NoModification(t *testing.T) {
|
||||
err := os.WriteFile(credsFile, []byte(contents), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
err = events.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
err = vcs.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
Ok(t, err)
|
||||
actContents, err := os.ReadFile(filepath.Join(tmp, ".git-credentials"))
|
||||
Ok(t, err)
|
||||
@@ -65,6 +66,7 @@ func TestWriteGitCreds_NoModification(t *testing.T) {
|
||||
|
||||
// Test that the github app credentials get replaced.
|
||||
func TestWriteGitCreds_ReplaceApp(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
@@ -72,7 +74,7 @@ func TestWriteGitCreds_ReplaceApp(t *testing.T) {
|
||||
err := os.WriteFile(credsFile, []byte(contents), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
err = events.WriteGitCreds("x-access-token", "token", "github.com", tmp, logger, true)
|
||||
err = vcs.WriteGitCreds("x-access-token", "token", "github.com", tmp, logger, true)
|
||||
Ok(t, err)
|
||||
expContets := "line1\nhttps://x-access-token:token@github.com\nline2"
|
||||
actContents, err := os.ReadFile(filepath.Join(tmp, ".git-credentials"))
|
||||
@@ -82,6 +84,7 @@ func TestWriteGitCreds_ReplaceApp(t *testing.T) {
|
||||
|
||||
// Test that the github app credentials get updated when cred file is empty.
|
||||
func TestWriteGitCreds_AppendApp(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
@@ -89,7 +92,7 @@ func TestWriteGitCreds_AppendApp(t *testing.T) {
|
||||
err := os.WriteFile(credsFile, []byte(contents), 0600)
|
||||
Ok(t, err)
|
||||
|
||||
err = events.WriteGitCreds("x-access-token", "token", "github.com", tmp, logger, true)
|
||||
err = vcs.WriteGitCreds("x-access-token", "token", "github.com", tmp, logger, true)
|
||||
Ok(t, err)
|
||||
expContets := "https://x-access-token:token@github.com"
|
||||
actContents, err := os.ReadFile(filepath.Join(tmp, ".git-credentials"))
|
||||
@@ -100,6 +103,7 @@ func TestWriteGitCreds_AppendApp(t *testing.T) {
|
||||
// Test that if we can't read the existing file to see if the contents will be
|
||||
// the same that we just error out.
|
||||
func TestWriteGitCreds_ErrIfCannotRead(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
credsFile := filepath.Join(tmp, ".git-credentials")
|
||||
@@ -107,23 +111,25 @@ func TestWriteGitCreds_ErrIfCannotRead(t *testing.T) {
|
||||
Ok(t, err)
|
||||
|
||||
expErr := fmt.Sprintf("open %s: permission denied", credsFile)
|
||||
actErr := events.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
actErr := vcs.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
ErrContains(t, expErr, actErr)
|
||||
}
|
||||
|
||||
// Test that if we can't write, we error out.
|
||||
func TestWriteGitCreds_ErrIfCannotWrite(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
credsFile := "/this/dir/does/not/exist/.git-credentials" // nolint: gosec
|
||||
expErr := fmt.Sprintf("writing generated .git-credentials file with user, token and hostname to %s: open %s: no such file or directory", credsFile, credsFile)
|
||||
actErr := events.WriteGitCreds("user", "token", "hostname", "/this/dir/does/not/exist", logger, false)
|
||||
actErr := vcs.WriteGitCreds("user", "token", "hostname", "/this/dir/does/not/exist", logger, false)
|
||||
ErrEquals(t, expErr, actErr)
|
||||
}
|
||||
|
||||
// Test that git is actually configured to use the credentials
|
||||
func TestWriteGitCreds_ConfigureGitCredentialHelper(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
err := events.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
err := vcs.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
Ok(t, err)
|
||||
|
||||
expOutput := `store`
|
||||
@@ -134,9 +140,10 @@ func TestWriteGitCreds_ConfigureGitCredentialHelper(t *testing.T) {
|
||||
|
||||
// Test that git is configured to use https instead of ssh
|
||||
func TestWriteGitCreds_ConfigureGitUrlOverride(t *testing.T) {
|
||||
logger := logging.NewNoopLogger(t)
|
||||
tmp := t.TempDir()
|
||||
|
||||
err := events.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
err := vcs.WriteGitCreds("user", "token", "hostname", tmp, logger, false)
|
||||
Ok(t, err)
|
||||
|
||||
expOutput := `ssh://git@hostname`
|
||||
@@ -16,7 +16,7 @@ type ExecutorService struct {
|
||||
log logging.SimpleLogging
|
||||
|
||||
// jobs
|
||||
runtimeStatsPublisher JobDefinition
|
||||
jobs []JobDefinition
|
||||
}
|
||||
|
||||
func NewExecutorService(
|
||||
@@ -33,11 +33,15 @@ func NewExecutorService(
|
||||
}
|
||||
|
||||
return &ExecutorService{
|
||||
log: log,
|
||||
runtimeStatsPublisher: runtimeStatsPublisherJob,
|
||||
log: log,
|
||||
jobs: []JobDefinition{runtimeStatsPublisherJob},
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ExecutorService) AddJob(jd JobDefinition) {
|
||||
s.jobs = append(s.jobs, jd)
|
||||
}
|
||||
|
||||
type JobDefinition struct {
|
||||
Job Job
|
||||
Period time.Duration
|
||||
@@ -50,7 +54,9 @@ func (s *ExecutorService) Run() {
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
s.runScheduledJob(ctx, &wg, s.runtimeStatsPublisher)
|
||||
for _, jd := range s.jobs {
|
||||
s.runScheduledJob(ctx, &wg, jd)
|
||||
}
|
||||
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
|
||||
@@ -96,6 +102,7 @@ func (s *ExecutorService) runScheduledJob(ctx context.Context, wg *sync.WaitGrou
|
||||
|
||||
}
|
||||
|
||||
//go:generate pegomock generate -m --package mocks -o mocks/mock_executor_service_job.go Job
|
||||
type Job interface {
|
||||
Run()
|
||||
}
|
||||
|
||||
48
server/scheduled/executor_service_test.go
Normal file
48
server/scheduled/executor_service_test.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package scheduled
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/petergtz/pegomock"
|
||||
"github.com/runatlantis/atlantis/server/logging"
|
||||
"github.com/runatlantis/atlantis/server/scheduled/mocks"
|
||||
)
|
||||
|
||||
func TestExecutorService_Run(t *testing.T) {
|
||||
pegomock.RegisterMockTestingT(t)
|
||||
mockJob := mocks.NewMockJob()
|
||||
type fields struct {
|
||||
log logging.SimpleLogging
|
||||
jobs []JobDefinition
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
}{
|
||||
{
|
||||
name: "test",
|
||||
fields: fields{
|
||||
log: logging.NewNoopLogger(t),
|
||||
jobs: []JobDefinition{
|
||||
{
|
||||
Job: mockJob,
|
||||
Period: 1 * time.Second,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := &ExecutorService{
|
||||
log: tt.fields.log,
|
||||
jobs: make([]JobDefinition, 0),
|
||||
}
|
||||
s.AddJob(tt.fields.jobs[0])
|
||||
go s.Run()
|
||||
time.Sleep(1050 * time.Millisecond)
|
||||
mockJob.VerifyWasCalledOnce().Run()
|
||||
})
|
||||
}
|
||||
}
|
||||
87
server/scheduled/mocks/mock_executor_service_job.go
Normal file
87
server/scheduled/mocks/mock_executor_service_job.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// Code generated by pegomock. DO NOT EDIT.
|
||||
// Source: github.com/runatlantis/atlantis/server/scheduled (interfaces: Job)
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
pegomock "github.com/petergtz/pegomock"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
type MockJob struct {
|
||||
fail func(message string, callerSkip ...int)
|
||||
}
|
||||
|
||||
func NewMockJob(options ...pegomock.Option) *MockJob {
|
||||
mock := &MockJob{}
|
||||
for _, option := range options {
|
||||
option.Apply(mock)
|
||||
}
|
||||
return mock
|
||||
}
|
||||
|
||||
func (mock *MockJob) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
|
||||
func (mock *MockJob) FailHandler() pegomock.FailHandler { return mock.fail }
|
||||
|
||||
func (mock *MockJob) Run() {
|
||||
if mock == nil {
|
||||
panic("mock must not be nil. Use myMock := NewMockJob().")
|
||||
}
|
||||
params := []pegomock.Param{}
|
||||
pegomock.GetGenericMockFrom(mock).Invoke("Run", params, []reflect.Type{})
|
||||
}
|
||||
|
||||
func (mock *MockJob) VerifyWasCalledOnce() *VerifierMockJob {
|
||||
return &VerifierMockJob{
|
||||
mock: mock,
|
||||
invocationCountMatcher: pegomock.Times(1),
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockJob) VerifyWasCalled(invocationCountMatcher pegomock.InvocationCountMatcher) *VerifierMockJob {
|
||||
return &VerifierMockJob{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockJob) VerifyWasCalledInOrder(invocationCountMatcher pegomock.InvocationCountMatcher, inOrderContext *pegomock.InOrderContext) *VerifierMockJob {
|
||||
return &VerifierMockJob{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
inOrderContext: inOrderContext,
|
||||
}
|
||||
}
|
||||
|
||||
func (mock *MockJob) VerifyWasCalledEventually(invocationCountMatcher pegomock.InvocationCountMatcher, timeout time.Duration) *VerifierMockJob {
|
||||
return &VerifierMockJob{
|
||||
mock: mock,
|
||||
invocationCountMatcher: invocationCountMatcher,
|
||||
timeout: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
type VerifierMockJob struct {
|
||||
mock *MockJob
|
||||
invocationCountMatcher pegomock.InvocationCountMatcher
|
||||
inOrderContext *pegomock.InOrderContext
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func (verifier *VerifierMockJob) Run() *MockJob_Run_OngoingVerification {
|
||||
params := []pegomock.Param{}
|
||||
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Run", params, verifier.timeout)
|
||||
return &MockJob_Run_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
|
||||
}
|
||||
|
||||
type MockJob_Run_OngoingVerification struct {
|
||||
mock *MockJob
|
||||
methodInvocations []pegomock.MethodInvocation
|
||||
}
|
||||
|
||||
func (c *MockJob_Run_OngoingVerification) GetCapturedArguments() {
|
||||
}
|
||||
|
||||
func (c *MockJob_Run_OngoingVerification) GetAllCapturedArguments() {
|
||||
}
|
||||
@@ -298,18 +298,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
}
|
||||
}
|
||||
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting home dir to write ~/.git-credentials file")
|
||||
}
|
||||
|
||||
if userConfig.WriteGitCreds {
|
||||
home, err := homedir.Dir()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "getting home dir to write ~/.git-credentials file")
|
||||
}
|
||||
if userConfig.GithubUser != "" {
|
||||
if err := events.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, logger, false); err != nil {
|
||||
if err := vcs.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, logger, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userConfig.GitlabUser != "" {
|
||||
if err := events.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, logger, false); err != nil {
|
||||
if err := vcs.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, logger, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -320,12 +321,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
if bitbucketBaseURL == "https://api.bitbucket.org" {
|
||||
bitbucketBaseURL = "bitbucket.org"
|
||||
}
|
||||
if err := events.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, bitbucketBaseURL, home, logger, false); err != nil {
|
||||
if err := vcs.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, bitbucketBaseURL, home, logger, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if userConfig.AzureDevopsUser != "" {
|
||||
if err := events.WriteGitCreds(userConfig.AzureDevopsUser, userConfig.AzureDevopsToken, "dev.azure.com", home, logger, false); err != nil {
|
||||
if err := vcs.WriteGitCreds(userConfig.AzureDevopsUser, userConfig.AzureDevopsToken, "dev.azure.com", home, logger, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -461,6 +462,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
CheckoutDepth: userConfig.CheckoutDepth,
|
||||
GithubAppEnabled: githubAppEnabled,
|
||||
}
|
||||
|
||||
scheduledExecutorService := scheduled.NewExecutorService(
|
||||
statsScope,
|
||||
logger,
|
||||
)
|
||||
|
||||
// provide fresh tokens before clone from the GitHub Apps integration, proxy workingDir
|
||||
if githubAppEnabled {
|
||||
if !userConfig.WriteGitCreds {
|
||||
@@ -471,6 +478,13 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
Credentials: githubCredentials,
|
||||
GithubHostname: userConfig.GithubHostname,
|
||||
}
|
||||
|
||||
githubAppTokenRotator := vcs.NewGithubAppTokenRotator(logger, githubCredentials, userConfig.GithubHostname, home)
|
||||
tokenJd, err := githubAppTokenRotator.GenerateJob()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "could not write credentials")
|
||||
}
|
||||
scheduledExecutorService.AddJob(tokenJd)
|
||||
}
|
||||
|
||||
projectLocker := &events.DefaultProjectLocker{
|
||||
@@ -861,10 +875,6 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
|
||||
GithubHostname: userConfig.GithubHostname,
|
||||
GithubOrg: userConfig.GithubOrg,
|
||||
}
|
||||
scheduledExecutorService := scheduled.NewExecutorService(
|
||||
statsScope,
|
||||
logger,
|
||||
)
|
||||
|
||||
return &Server{
|
||||
AtlantisVersion: config.AtlantisVersion,
|
||||
|
||||
Reference in New Issue
Block a user