Skip to content

Commit

Permalink
Fixed snapshot version/content check before push to repo
Browse files Browse the repository at this point in the history
  • Loading branch information
petruki committed Sep 11, 2024
1 parent 2bd04ee commit b1ec1c2
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 23 deletions.
7 changes: 7 additions & 0 deletions resources/fixtures/api/default_snapshot_version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"data": {
"domain": {
"version": 1
}
}
}
38 changes: 36 additions & 2 deletions src/core/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type ApplyChangeResponse struct {
}

type IAPIService interface {
FetchSnapshotVersion(domainId string, environment string) (string, error)
FetchSnapshot(domainId string, environment string) (string, error)
ApplyChangesToAPI(domainId string, environment string, diff model.DiffResult) (ApplyChangeResponse, error)
NewDataFromJson(jsonData []byte) model.Data
Expand All @@ -45,6 +46,32 @@ func (c *ApiService) NewDataFromJson(jsonData []byte) model.Data {
return data
}

func (a *ApiService) FetchSnapshotVersion(domainId string, environment string) (string, error) {
// Generate a bearer token
token := generateBearerToken(a.ApiKey, domainId)

// Define the GraphQL query
query := createQuerySnapshotVersion(domainId)

// Create a new request
reqBody, _ := json.Marshal(GraphQLRequest{Query: query})
req, _ := http.NewRequest("POST", a.ApiUrl+"/gitops-graphql", bytes.NewBuffer(reqBody))

// Set the request headers
setHeaders(req, token)

// Send the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()

body, _ := io.ReadAll(resp.Body)
return string(body), nil
}

func (a *ApiService) FetchSnapshot(domainId string, environment string) (string, error) {
// Generate a bearer token
token := generateBearerToken(a.ApiKey, domainId)
Expand All @@ -67,7 +94,6 @@ func (a *ApiService) FetchSnapshot(domainId string, environment string) (string,
}
defer resp.Body.Close()

// Read and print the response
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
Expand All @@ -91,7 +117,6 @@ func (a *ApiService) ApplyChangesToAPI(domainId string, environment string, diff
}
defer resp.Body.Close()

// Read and print the response
body, _ := io.ReadAll(resp.Body)
var response ApplyChangeResponse
json.Unmarshal(body, &response)
Expand Down Expand Up @@ -121,6 +146,15 @@ func setHeaders(req *http.Request, token string) {
req.Header.Set("Authorization", "Bearer "+token)
}

func createQuerySnapshotVersion(domainId string) string {
return fmt.Sprintf(`
{
domain(_id: "%s") {
version
}
}`, domainId)
}

func createQuery(domainId string, environment string) string {
return fmt.Sprintf(`
{
Expand Down
42 changes: 42 additions & 0 deletions src/core/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,48 @@ import (

const SWITCHER_API_JWT_SECRET = "SWITCHER_API_JWT_SECRET"

func TestFetchSnapshotVersion(t *testing.T) {
t.Run("Should return snapshot version", func(t *testing.T) {
responsePayload := utils.ReadJsonFromFile("../../resources/fixtures/api/default_snapshot_version.json")
fakeApiServer := givenApiResponse(http.StatusOK, responsePayload)
defer fakeApiServer.Close()

apiService := NewApiService(SWITCHER_API_JWT_SECRET, fakeApiServer.URL)
version, _ := apiService.FetchSnapshotVersion("domainId", "default")

assert.Contains(t, version, "version", "Missing version in response")
assert.Contains(t, version, "domain", "Missing domain in response")
})

t.Run("Should return error - invalid API key", func(t *testing.T) {
fakeApiServer := givenApiResponse(http.StatusUnauthorized, `{ "error": "Invalid API token" }`)
defer fakeApiServer.Close()

apiService := NewApiService("INVALID_KEY", fakeApiServer.URL)
version, _ := apiService.FetchSnapshotVersion("domainId", "default")

assert.Contains(t, version, "Invalid API token")
})

t.Run("Should return error - invalid domain", func(t *testing.T) {
responsePayload := utils.ReadJsonFromFile("../../resources/fixtures/api/error_invalid_domain.json")
fakeApiServer := givenApiResponse(http.StatusUnauthorized, responsePayload)
defer fakeApiServer.Close()

apiService := NewApiService(SWITCHER_API_JWT_SECRET, fakeApiServer.URL)
version, _ := apiService.FetchSnapshotVersion("INVALID_DOMAIN", "default")

assert.Contains(t, version, "errors")
})

t.Run("Should return error - invalid API URL", func(t *testing.T) {
apiService := NewApiService(config.GetEnv(SWITCHER_API_JWT_SECRET), "http://localhost:8080")
_, err := apiService.FetchSnapshotVersion("domainId", "default")

assert.NotNil(t, err)
})
}

func TestFetchSnapshot(t *testing.T) {
t.Run("Should return snapshot", func(t *testing.T) {
responsePayload := utils.ReadJsonFromFile("../../resources/fixtures/api/default_snapshot.json")
Expand Down
41 changes: 29 additions & 12 deletions src/core/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,18 @@ func (c *CoreHandler) StartAccountHandler(accountId string, gitService IGitServi
continue
}

// Print account domain version
utils.Log(utils.LogLevelDebug, "[%s] Repository data: %s", accountId, utils.FormatJSON(repositoryData.Content))
// Fetch snapshot version from API
snapshotVersionPayload, err := c.ApiService.FetchSnapshotVersion(account.Domain.ID, account.Environment)

if err != nil {
utils.Log(utils.LogLevelError, "[%s] Failed to fetch snapshot version - %s", accountId, err.Error())
c.updateDomainStatus(*account, model.StatusError, "Failed to fetch snapshot version - "+err.Error())
time.Sleep(1 * time.Minute)
continue
}

// Check if repository is out of sync
if isRepositoryOutSync(*account, repositoryData.CommitHash) {
if c.isRepositoryOutSync(*account, repositoryData.CommitHash, snapshotVersionPayload) {
c.syncUp(*account, repositoryData, gitService)
}

Expand Down Expand Up @@ -126,7 +133,13 @@ func (c *CoreHandler) syncUp(account model.Account, repositoryData *model.Reposi
changeSource := ""
if snapshotApi.Domain.Version > account.Domain.Version {
changeSource = "Repository"
account, err = c.applyChangesToRepository(account, snapshotApi, gitService)
if len(diff.Changes) > 0 {
account, err = c.applyChangesToRepository(account, snapshotApi, gitService)
} else {
utils.Log(utils.LogLevelInfo, "[%s] Repository is up to date", account.ID.Hex())
account.Domain.Version = snapshotApi.Domain.Version
account.Domain.LastCommit = repositoryData.CommitHash
}
} else if len(diff.Changes) > 0 {
changeSource = "API"
account = c.applyChangesToAPI(account, repositoryData)
Expand Down Expand Up @@ -198,16 +211,15 @@ func (c *CoreHandler) applyChangesToRepository(account model.Account, snapshot m
return account, err
}

func isRepositoryOutSync(account model.Account, lastCommit string) bool {
utils.Log(utils.LogLevelDebug, "[%s] Checking account - Last commit: %s - Domain Version: %d",
account.ID.Hex(), account.Domain.LastCommit, account.Domain.Version)
func (c *CoreHandler) isRepositoryOutSync(account model.Account, lastCommit string, snapshotVersionPayload string) bool {
snapshotVersion := c.ApiService.NewDataFromJson([]byte(snapshotVersionPayload)).Snapshot.Domain.Version

return account.Domain.LastCommit == "" || account.Domain.LastCommit != lastCommit
}
utils.Log(utils.LogLevelDebug, "[%s] Checking account - Last commit: %s - Domain Version: %d - Snapshot Version: %d",
account.ID.Hex(), account.Domain.LastCommit, account.Domain.Version, snapshotVersion)

func getTimeWindow(window string) (int, time.Duration) {
duration, _ := time.ParseDuration(window)
return 1, duration
return account.Domain.LastCommit == "" || // First sync
account.Domain.LastCommit != lastCommit || // Repository out of sync
account.Domain.Version != snapshotVersion // API out of sync
}

func (c *CoreHandler) updateDomainStatus(account model.Account, status string, message string) {
Expand All @@ -217,3 +229,8 @@ func (c *CoreHandler) updateDomainStatus(account model.Account, status string, m
account.Domain.LastDate = time.Now().Format(time.ANSIC)
c.AccountRepository.Update(&account)
}

func getTimeWindow(window string) (int, time.Duration) {
duration, _ := time.ParseDuration(window)
return 1, duration
}
105 changes: 97 additions & 8 deletions src/core/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,32 @@ func TestStartAccountHandler(t *testing.T) {
tearDown()
})

t.Run("Should not sync when fetch snapshot version returns an error", func(t *testing.T) {
// Given
fakeGitService := NewFakeGitService()
fakeApiService := NewFakeApiService()
fakeApiService.throwErrorVersion = true

coreHandler = NewCoreHandler(coreHandler.AccountRepository, fakeApiService, NewComparatorService())

account := givenAccount()
account.Domain.ID = "123-error-fetch-snapshot-version"
accountCreated, _ := coreHandler.AccountRepository.Create(&account)

// Test
go coreHandler.StartAccountHandler(accountCreated.ID.Hex(), fakeGitService)

time.Sleep(1 * time.Second)

// Assert
accountFromDb, _ := coreHandler.AccountRepository.FetchByDomainId(accountCreated.Domain.ID)
assert.Equal(t, model.StatusError, accountFromDb.Domain.Status)
assert.Contains(t, accountFromDb.Domain.Message, "Failed to fetch snapshot version")
assert.Equal(t, "", accountFromDb.Domain.LastCommit)

tearDown()
})

t.Run("Should not sync after account is deleted", func(t *testing.T) {
// Given
fakeGitService := NewFakeGitService()
Expand Down Expand Up @@ -159,6 +185,51 @@ func TestStartAccountHandler(t *testing.T) {
tearDown()
})

t.Run("Should sync successfully when repository is up to date but not synced", func(t *testing.T) {
// Given
fakeGitService := NewFakeGitService()
fakeGitService.content = `{
"domain": {
"group": [{
"name": "Release 1",
"description": "Showcase configuration",
"activated": true,
"config": [{
"key": "MY_SWITCHER",
"description": "",
"activated": true,
"strategies": [],
"components": [
"switcher-playground"
]
}]
}]
}
}`
fakeApiService := NewFakeApiService()
coreHandler = NewCoreHandler(coreHandler.AccountRepository, fakeApiService, NewComparatorService())

account := givenAccount()
account.Domain.ID = "123-up-to-date-not-synced"
accountCreated, _ := coreHandler.AccountRepository.Create(&account)

// Test
go coreHandler.StartAccountHandler(accountCreated.ID.Hex(), fakeGitService)

// Wait for goroutine to process
time.Sleep(1 * time.Second)

// Assert
accountFromDb, _ := coreHandler.AccountRepository.FetchByDomainId(accountCreated.Domain.ID)
assert.Equal(t, model.StatusSynced, accountFromDb.Domain.Status)
assert.Contains(t, accountFromDb.Domain.Message, model.MessageSynced)
assert.Equal(t, "123", accountFromDb.Domain.LastCommit)
assert.Equal(t, 1, accountFromDb.Domain.Version)
assert.NotEqual(t, "", accountFromDb.Domain.LastDate)

tearDown()
})

t.Run("Should sync and prune successfully when repository is out of sync", func(t *testing.T) {
// Given
fakeGitService := NewFakeGitService()
Expand Down Expand Up @@ -226,7 +297,7 @@ func TestStartAccountHandler(t *testing.T) {
// Given
fakeGitService := NewFakeGitService()
fakeApiService := NewFakeApiService()
fakeApiService.throwError = true
fakeApiService.throwErrorSnapshot = true

coreHandler = NewCoreHandler(coreHandler.AccountRepository, fakeApiService, NewComparatorService())

Expand Down Expand Up @@ -349,14 +420,24 @@ func (f *FakeGitService) UpdateRepositorySettings(repository string, token strin
}

type FakeApiService struct {
throwError bool
response string
throwErrorVersion bool
throwErrorSnapshot bool
responseVersion string
responseSnapshot string
}

func NewFakeApiService() *FakeApiService {
return &FakeApiService{
throwError: false,
response: `{
throwErrorVersion: false,
throwErrorSnapshot: false,
responseVersion: `{
"data": {
"domain": {
"version": 1
}
}
}`,
responseSnapshot: `{
"data": {
"domain": {
"name": "Switcher GitOps",
Expand All @@ -381,12 +462,20 @@ func NewFakeApiService() *FakeApiService {
}
}

func (f *FakeApiService) FetchSnapshotVersion(domainId string, environment string) (string, error) {
if f.throwErrorVersion {
return "", errors.New("Something went wrong")
}

return f.responseVersion, nil
}

func (f *FakeApiService) FetchSnapshot(domainId string, environment string) (string, error) {
if f.throwError {
return "", assert.AnError
if f.throwErrorSnapshot {
return "", errors.New("Something went wrong")
}

return f.response, nil
return f.responseSnapshot, nil
}

func (f *FakeApiService) ApplyChangesToAPI(domainId string, environment string, diff model.DiffResult) (ApplyChangeResponse, error) {
Expand Down
3 changes: 2 additions & 1 deletion src/utils/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ const (
func Log(logLevel string, message string, args ...interface{}) {
currentLogLevel := config.GetEnv("LOG_LEVEL")

if logLevel == LogLevelDebug || logLevel == LogLevelError || currentLogLevel == logLevel {
if currentLogLevel == LogLevelDebug || currentLogLevel == LogLevelError ||
currentLogLevel == logLevel || LogLevelError == logLevel {
log.Printf("[%s] %s\n", logLevel, fmt.Sprintf(message, args...))
}
}
Expand Down

0 comments on commit b1ec1c2

Please sign in to comment.