From 5bd629b64ef94b77ef48bfe5be7f9b4c55b33e9e Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sat, 24 Aug 2024 10:54:34 -0700 Subject: [PATCH] Added GitService::PushChanges and integrated with handler --- .env.test | 1 + .github/workflows/master.yml | 1 + .vscode/settings.json | 2 +- src/core/core_test.go | 1 + src/core/git.go | 82 ++++++++++++++++-------- src/core/git_test.go | 117 ++++++++++++++++++++++++++++++++--- src/core/handler.go | 16 +++-- src/core/handler_test.go | 8 +-- 8 files changed, 182 insertions(+), 46 deletions(-) diff --git a/.env.test b/.env.test index a711dee..4c1c75d 100644 --- a/.env.test +++ b/.env.test @@ -8,5 +8,6 @@ SWITCHER_API_JWT_SECRET=[YOUR_JWT_SECRET] # Only for testing purposes. Values are loaded from accounts API_DOMAIN_ID= GIT_TOKEN= +GIT_TOKEN_READ_ONLY= GIT_REPO_URL=https://github.com/switcherapi/switcher-gitops-fixture GIT_BRANCH=main \ No newline at end of file diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 4f012bb..e0a0c29 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -40,6 +40,7 @@ jobs: SWITCHER_API_JWT_SECRET: ${{ secrets.SWITCHER_API_JWT_SECRET }} API_DOMAIN_ID: ${{ secrets.API_DOMAIN_ID }} GIT_TOKEN: ${{ secrets.GIT_TOKEN }} + GIT_TOKEN_READ_ONLY: ${{ secrets.GIT_TOKEN_READ_ONLY }} GIT_REPO_URL: ${{ secrets.GIT_REPO_URL }} GIT_BRANCH: ${{ secrets.GIT_BRANCH }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 8600625..218af21 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,3 @@ { - "go.testFlags": ["-v"], + "go.testFlags": ["-v", "clean -testcache"], } \ No newline at end of file diff --git a/src/core/core_test.go b/src/core/core_test.go index d73635b..0910eb2 100644 --- a/src/core/core_test.go +++ b/src/core/core_test.go @@ -45,6 +45,7 @@ func shutdown() { func canRunIntegratedTests() bool { return config.GetEnv("GIT_REPO_URL") != "" && config.GetEnv("GIT_TOKEN") != "" && + config.GetEnv("GIT_TOKEN_READ_ONLY") != "" && config.GetEnv("GIT_BRANCH") != "" && config.GetEnv("API_DOMAIN_ID") != "" } diff --git a/src/core/git.go b/src/core/git.go index ec29888..a394ead 100644 --- a/src/core/git.go +++ b/src/core/git.go @@ -3,6 +3,7 @@ package core import ( "time" + "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" git "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" @@ -14,6 +15,7 @@ import ( type IGitService interface { GetRepositoryData(environment string) (*model.RepositoryData, error) + PushChanges(environment string, content string) (string, error) } type GitService struct { @@ -44,6 +46,51 @@ func (g *GitService) GetRepositoryData(environment string) (*model.RepositoryDat }, nil } +func (g *GitService) PushChanges(environment string, content string) (string, error) { + // Create an in-memory file system + fs := memfs.New() + + // Get the repository & Clone repository using in-memory storage + r, _ := g.getRepository(fs) + + // Write the content to the in-memory file + filePath := model.FilePath + environment + ".json" + file, _ := fs.Create(filePath) + file.Write([]byte(content)) + file.Close() + + // Get the worktree + w, _ := r.Worktree() + + // Add the file to the worktree + w.Add(filePath) + + // Commit the changes + commit, _ := w.Commit("[switcher-gitops] updated "+environment+".json", g.createCommitOptions()) + + // Push the changes + err := r.Push(&git.PushOptions{ + Force: true, + Auth: g.getAuth(), + }) + + if err != nil { + return "", err + } + + return commit.String(), nil +} + +func (g *GitService) createCommitOptions() *git.CommitOptions { + return &git.CommitOptions{ + Author: &object.Signature{ + Name: "Switcher GitOps", + Email: "switcher.gitops.no-reply@switcherapi.com", + When: time.Now(), + }, + } +} + func (g *GitService) getLastCommitData(filePath string) (string, time.Time, string, error) { c, err := g.getCommitObject() @@ -69,7 +116,7 @@ func (g *GitService) getLastCommitData(filePath string) (string, time.Time, stri } func (g *GitService) getCommitObject() (*object.Commit, error) { - r, err := g.getRepository() + r, err := g.getRepository(memfs.New()) if err != nil { return nil, err @@ -82,32 +129,17 @@ func (g *GitService) getCommitObject() (*object.Commit, error) { return r.CommitObject(ref.Hash()) } -func (g *GitService) getRepository() (*git.Repository, error) { - // Clone repository using in-memory storage - r, _ := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ - URL: g.RepoURL, - Auth: &http.BasicAuth{ - Username: "git-user", - Password: g.Token, - }, +func (g *GitService) getRepository(fs billy.Filesystem) (*git.Repository, error) { + return git.Clone(memory.NewStorage(), fs, &git.CloneOptions{ + URL: g.RepoURL, + ReferenceName: plumbing.NewBranchReferenceName(g.BranchName), + Auth: g.getAuth(), }) - - // Checkout branch - return g.checkoutBranch(*r) } -func (g *GitService) checkoutBranch(r git.Repository) (*git.Repository, error) { - // Fetch worktree - w, err := r.Worktree() - - if err != nil { - return nil, err +func (g *GitService) getAuth() *http.BasicAuth { + return &http.BasicAuth{ + Username: "git-user", + Password: g.Token, } - - // Checkout remote branch - err = w.Checkout(&git.CheckoutOptions{ - Branch: plumbing.NewRemoteReferenceName("origin", g.BranchName), - }) - - return &r, err } diff --git a/src/core/git_test.go b/src/core/git_test.go index cddcfb5..139329f 100644 --- a/src/core/git_test.go +++ b/src/core/git_test.go @@ -2,9 +2,16 @@ package core import ( "testing" - + "time" + + "github.com/go-git/go-billy/v5/memfs" + git "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/go-git/go-git/v5/storage/memory" "github.com/stretchr/testify/assert" - "github.com/switcherapi/switcher-gitops/src/config" + appConfig "github.com/switcherapi/switcher-gitops/src/config" ) const ( @@ -33,9 +40,9 @@ func TestGetRepositoryData(t *testing.T) { // Given gitService := NewGitService( - config.GetEnv("GIT_REPO_URL"), - config.GetEnv("GIT_TOKEN"), - config.GetEnv("GIT_BRANCH")) + appConfig.GetEnv("GIT_REPO_URL"), + appConfig.GetEnv("GIT_TOKEN"), + appConfig.GetEnv("GIT_BRANCH")) // Test repositoryData, err := gitService.GetRepositoryData("default") @@ -54,9 +61,9 @@ func TestGetRepositoryDataErrorInvalidEnvironment(t *testing.T) { // Given gitService := NewGitService( - config.GetEnv("GIT_REPO_URL"), - config.GetEnv("GIT_TOKEN"), - config.GetEnv("GIT_BRANCH")) + appConfig.GetEnv("GIT_REPO_URL"), + appConfig.GetEnv("GIT_TOKEN"), + appConfig.GetEnv("GIT_BRANCH")) // Test repositoryData, err := gitService.GetRepositoryData("invalid") @@ -73,9 +80,9 @@ func TestGetRepositoryDataErrorInvalidToken(t *testing.T) { // Given gitService := NewGitService( - config.GetEnv("GIT_REPO_URL"), + appConfig.GetEnv("GIT_REPO_URL"), "invalid", - config.GetEnv("GIT_BRANCH")) + appConfig.GetEnv("GIT_BRANCH")) // Test repositoryData, err := gitService.GetRepositoryData("default") @@ -84,3 +91,93 @@ func TestGetRepositoryDataErrorInvalidToken(t *testing.T) { assert.NotNil(t, err) assert.Nil(t, repositoryData) } + +func TestPushChanges(t *testing.T) { + if !canRunIntegratedTests() { + t.Skip(SkipMessage) + } + + t.Run("Should push changes to repository", func(t *testing.T) { + // Given + branchName := "test-branch-" + time.Now().Format("20060102150405") + createBranch(branchName) + gitService := NewGitService( + appConfig.GetEnv("GIT_REPO_URL"), + appConfig.GetEnv("GIT_TOKEN"), + branchName) + + // Test + commitHash, err := gitService.PushChanges("default", "content") + + // Assert + assert.Nil(t, err) + assert.NotEmpty(t, commitHash) + + data, _ := gitService.GetRepositoryData("default") + assert.Equal(t, commitHash, data.CommitHash) + assert.Equal(t, "content", data.Content) + + // Clean up + deleteBranch(branchName) + }) + + t.Run("Should fail to push changes to repository - no write access", func(t *testing.T) { + // Given + gitService := NewGitService( + appConfig.GetEnv("GIT_REPO_URL"), + appConfig.GetEnv("GIT_TOKEN_READ_ONLY"), + appConfig.GetEnv("GIT_BRANCH")) + + // Test + commitHash, err := gitService.PushChanges("default", "content") + + // Assert + assert.NotNil(t, err) + assert.Equal(t, "authorization failed", err.Error()) + assert.Empty(t, commitHash) + }) +} + +// Helpers + +func createBranch(branchName string) { + repo, _ := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ + URL: appConfig.GetEnv("GIT_REPO_URL"), + Auth: getAuth(), + }) + + wt, _ := repo.Worktree() + + head, _ := repo.Head() + + wt.Checkout(&git.CheckoutOptions{ + Branch: plumbing.NewBranchReferenceName(branchName), + Hash: head.Hash(), + Create: true, + }) + + repo.Push(&git.PushOptions{ + RemoteName: "origin", + Auth: getAuth(), + RefSpecs: []config.RefSpec{config.RefSpec("refs/heads/" + branchName + ":refs/heads/" + branchName)}, + }) +} + +func deleteBranch(branchName string) { + r, _ := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ + URL: appConfig.GetEnv("GIT_REPO_URL"), + Auth: getAuth(), + }) + + r.Push(&git.PushOptions{ + Auth: getAuth(), + RefSpecs: []config.RefSpec{config.RefSpec(":refs/heads/" + branchName)}, + }) +} + +func getAuth() *http.BasicAuth { + return &http.BasicAuth{ + Username: "git-user", + Password: appConfig.GetEnv("GIT_TOKEN"), + } +} diff --git a/src/core/handler.go b/src/core/handler.go index 6c6a539..a92bd27 100644 --- a/src/core/handler.go +++ b/src/core/handler.go @@ -6,6 +6,7 @@ import ( "github.com/switcherapi/switcher-gitops/src/model" "github.com/switcherapi/switcher-gitops/src/repository" + "github.com/switcherapi/switcher-gitops/src/utils" ) type CoreHandler struct { @@ -81,7 +82,7 @@ func (c *CoreHandler) syncUp(account model.Account, repositoryData *model.Reposi } if snapshotApi.Domain.Version > account.Domain.Version { - account = c.applyChangesToRepository(account, snapshotApi.Domain) + account = c.applyChangesToRepository(account, snapshotApi) } else if len(diff.Changes) > 0 { account = c.applyChangesToAPI(account, repositoryData) } @@ -125,13 +126,16 @@ func (c *CoreHandler) applyChangesToAPI(account model.Account, repositoryData *m return account } -func (c *CoreHandler) applyChangesToRepository(account model.Account, domain model.Domain) model.Account { - // Push changes to repository - println("Pushing changes to repository") +func (c *CoreHandler) applyChangesToRepository(account model.Account, snapshot model.Snapshot) model.Account { + // Remove version from domain + snapshotContent := snapshot + snapshotContent.Domain.Version = "" + + lastCommit, _ := c.GitService.PushChanges(account.Environment, utils.ToJsonFromObject(snapshotContent)) // Update domain - account.Domain.Version = domain.Version - account.Domain.LastCommit = "111" + account.Domain.Version = snapshot.Domain.Version + account.Domain.LastCommit = lastCommit return account } diff --git a/src/core/handler_test.go b/src/core/handler_test.go index e059d09..ca8f375 100644 --- a/src/core/handler_test.go +++ b/src/core/handler_test.go @@ -94,6 +94,8 @@ func TestStartAccountHandler(t *testing.T) { t.Run("Should sync successfully when API has a newer version", func(t *testing.T) { // Given fakeGitService := NewFakeGitService() + fakeGitService.lastCommit = "111" + fakeApiService := NewFakeApiService(false) coreHandler = NewCoreHandler(coreHandler.AccountRepository, fakeGitService, fakeApiService, NewComparatorService()) @@ -172,7 +174,6 @@ type FakeGitService struct { date string content string status string - message string } func NewFakeGitService() *FakeGitService { @@ -209,9 +210,8 @@ func (f *FakeGitService) GetRepositoryData(environment string) (*model.Repositor }, nil } -func (f *FakeGitService) CheckForChanges(account model.Account, lastCommit string, - date string, content string) (status string, message string) { - return f.status, f.message +func (f *FakeGitService) PushChanges(environment string, content string) (string, error) { + return f.lastCommit, nil } type FakeApiService struct {