Skip to content

Commit 5fa9655

Browse files
Almenonijames-gc
authored andcommitted
fix(bitbucket): avoid extra plans using a local LRU (runatlantis#3402)
1 parent 736501f commit 5fa9655

File tree

8 files changed

+68
-6
lines changed

8 files changed

+68
-6
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,6 @@ dist/
2424
tmp-CHANGELOG.md
2525

2626
.envrc
27+
28+
# IDE files
29+
*.code-workspace

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ require (
2020
github.com/hashicorp/go-getter/v2 v2.2.1
2121
github.com/hashicorp/go-multierror v1.1.1
2222
github.com/hashicorp/go-version v1.6.0
23+
github.com/hashicorp/golang-lru/v2 v2.0.2
2324
github.com/hashicorp/terraform-config-inspect v0.0.0-20230614215431-f32df32a01cd
2425
github.com/kr/pretty v0.3.1
2526
github.com/mcdafydd/go-azuredevops v0.12.1

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mO
253253
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
254254
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
255255
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
256+
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
257+
github.com/hashicorp/golang-lru/v2 v2.0.2 h1:Dwmkdr5Nc/oBiXgJS3CDHNhJtIHkuZ3DZF5twqnfBdU=
258+
github.com/hashicorp/golang-lru/v2 v2.0.2/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
256259
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
257260
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
258261
github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY=

runatlantis.io/docs/autoplanning.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ Given the directory structure:
3232
* If `project1/modules/module1/main.tf` were modified, we would look one level above `project1/modules`
3333
into `project1/`, see that there was a `main.tf` file and so run plan in `project1/`
3434

35+
## Bitbucket-Specific Notes
36+
Bitbucket does not have a webhook that triggers only upon a new PR or commit. To fix this we cache the last commit to see if it has changed. If the cache is emptied, Atlantis will think your commit is new and you may see extra plans.
37+
This scenario can happen if:
38+
* Atlantis restarts
39+
* You are running multiple Atlantis instances behind a load balancer
40+
3541
## Customizing
3642
If you would like to customize how Atlantis determines which directory to run in
3743
or disable it all together you need to create an `atlantis.yaml` file.

server/controllers/events/events_controller.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,8 @@ func (e *VCSEventsController) handleBitbucketCloudPullRequestEvent(w http.Respon
366366
e.respond(w, logging.Error, http.StatusBadRequest, "Error parsing pull data: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
367367
return
368368
}
369-
pullEventType := e.Parser.GetBitbucketCloudPullEventType(eventType)
369+
e.Logger.Debug("SHA is %q", pull.HeadCommit)
370+
pullEventType := e.Parser.GetBitbucketCloudPullEventType(eventType, pull.HeadCommit, pull.URL)
370371
e.Logger.Info("identified event as type %q", pullEventType.String())
371372
resp := e.handlePullRequestEvent(e.Logger, baseRepo, headRepo, pull, user, pullEventType)
372373

server/events/event_parser.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/go-playground/validator/v10"
2424
"github.com/google/go-github/v53/github"
25+
lru "github.com/hashicorp/golang-lru/v2"
2526
"github.com/mcdafydd/go-azuredevops/azuredevops"
2627
"github.com/pkg/errors"
2728
"github.com/runatlantis/atlantis/server/events/command"
@@ -34,6 +35,8 @@ import (
3435
const gitlabPullOpened = "opened"
3536
const usagesCols = 90
3637

38+
var lastBitbucketSha, _ = lru.New[string, string](300)
39+
3740
// PullCommand is a command to run on a pull request.
3841
type PullCommand interface {
3942
// CommandName is the name of the command we're running.
@@ -267,7 +270,7 @@ type EventParsing interface {
267270

268271
// GetBitbucketCloudPullEventType returns the type of the pull request
269272
// event given the Bitbucket Cloud header.
270-
GetBitbucketCloudPullEventType(eventTypeHeader string) models.PullRequestEventType
273+
GetBitbucketCloudPullEventType(eventTypeHeader string, sha string, pr string) models.PullRequestEventType
271274

272275
// ParseBitbucketServerPullEvent parses a pull request event from Bitbucket
273276
// Server.
@@ -343,11 +346,18 @@ func (e *EventParser) ParseAPIPlanRequest(vcsHostType models.VCSHostType, repoFu
343346

344347
// GetBitbucketCloudPullEventType returns the type of the pull request
345348
// event given the Bitbucket Cloud header.
346-
func (e *EventParser) GetBitbucketCloudPullEventType(eventTypeHeader string) models.PullRequestEventType {
349+
func (e *EventParser) GetBitbucketCloudPullEventType(eventTypeHeader string, sha string, pr string) models.PullRequestEventType {
347350
switch eventTypeHeader {
348351
case bitbucketcloud.PullCreatedHeader:
352+
lastBitbucketSha.Add(pr, sha)
349353
return models.OpenedPullEvent
350354
case bitbucketcloud.PullUpdatedHeader:
355+
lastSha, _ := lastBitbucketSha.Get(pr)
356+
if sha == lastSha {
357+
// No change, ignore
358+
return models.OtherPullEvent
359+
}
360+
lastBitbucketSha.Add(pr, sha)
351361
return models.UpdatedPullEvent
352362
case bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader:
353363
return models.ClosedPullEvent

server/events/event_parser_test.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -998,6 +998,42 @@ func TestParseBitbucketCloudPullEvent_States(t *testing.T) {
998998
}
999999
}
10001000

1001+
func TestBitBucketNonCodeChangesAreIgnored(t *testing.T) {
1002+
// lets say a user opens a PR
1003+
act := parser.GetBitbucketCloudPullEventType("pullrequest:created", "fakeSha", "https://github.com/fakeorg/fakerepo/pull/1")
1004+
Equals(t, models.OpenedPullEvent, act)
1005+
// Another update with same SHA should be ignored
1006+
act = parser.GetBitbucketCloudPullEventType("pullrequest:updated", "fakeSha", "https://github.com/fakeorg/fakerepo/pull/1")
1007+
Equals(t, models.OtherPullEvent, act)
1008+
// Only if SHA changes do we act
1009+
act = parser.GetBitbucketCloudPullEventType("pullrequest:updated", "fakeSha2", "https://github.com/fakeorg/fakerepo/pull/1")
1010+
Equals(t, models.UpdatedPullEvent, act)
1011+
1012+
// If sha changes in seperate PR,
1013+
act = parser.GetBitbucketCloudPullEventType("pullrequest:updated", "otherPRSha", "https://github.com/fakeorg/fakerepo/pull/2")
1014+
Equals(t, models.UpdatedPullEvent, act)
1015+
// We will still ignore same shas in first PR
1016+
act = parser.GetBitbucketCloudPullEventType("pullrequest:updated", "fakeSha2", "https://github.com/fakeorg/fakerepo/pull/1")
1017+
Equals(t, models.OtherPullEvent, act)
1018+
}
1019+
1020+
func TestBitbucketShaCacheExpires(t *testing.T) {
1021+
// lets say a user opens a PR
1022+
act := parser.GetBitbucketCloudPullEventType("pullrequest:created", "fakeSha", "https://github.com/fakeorg/fakerepo/pull/1")
1023+
Equals(t, models.OpenedPullEvent, act)
1024+
// Another update with same SHA should be ignored
1025+
act = parser.GetBitbucketCloudPullEventType("pullrequest:updated", "fakeSha", "https://github.com/fakeorg/fakerepo/pull/1")
1026+
Equals(t, models.OtherPullEvent, act)
1027+
// But after 300 times, the cache should expire
1028+
// this is so we don't have ever increasing memory usage
1029+
for i := 0; i < 302; i++ {
1030+
parser.GetBitbucketCloudPullEventType("pullrequest:updated", "fakeSha", fmt.Sprintf("https://github.com/fakeorg/fakerepo/pull/%d", i))
1031+
}
1032+
// and now SHA will seen as a change again
1033+
act = parser.GetBitbucketCloudPullEventType("pullrequest:updated", "fakeSha", "https://github.com/fakeorg/fakerepo/pull/1")
1034+
Equals(t, models.UpdatedPullEvent, act)
1035+
}
1036+
10011037
func TestGetBitbucketCloudEventType(t *testing.T) {
10021038
cases := []struct {
10031039
header string
@@ -1026,7 +1062,9 @@ func TestGetBitbucketCloudEventType(t *testing.T) {
10261062
}
10271063
for _, c := range cases {
10281064
t.Run(c.header, func(t *testing.T) {
1029-
act := parser.GetBitbucketCloudPullEventType(c.header)
1065+
// we pass in the header as the SHA so the SHA changes each time
1066+
// the code will ignore duplicate SHAS to avoid extra TF plans
1067+
act := parser.GetBitbucketCloudPullEventType(c.header, c.header, "https://github.com/fakeorg/fakerepo/pull/1")
10301068
Equals(t, c.exp, act)
10311069
})
10321070
}

server/events/mocks/mock_event_parsing.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)