diff --git a/CHANGELOG.md b/CHANGELOG.md index 5260bc4c..c38ffdcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## UNRELEASED +BUG FIXES: +* plugin/target/aws-asg: Ignore warm pool activities when determining ASG readiness [[GH-1151]](https://github.com/hashicorp/nomad-autoscaler/pull/1151) + ## 0.4.8 (November 13, 2025) IMPROVEMENTS: diff --git a/plugins/builtin/target/aws-asg/plugin/plugin.go b/plugins/builtin/target/aws-asg/plugin/plugin.go index 72f02eab..7b7bc9f3 100644 --- a/plugins/builtin/target/aws-asg/plugin/plugin.go +++ b/plugins/builtin/target/aws-asg/plugin/plugin.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "strconv" + "strings" "github.com/aws/aws-sdk-go-v2/service/autoscaling" "github.com/aws/aws-sdk-go-v2/service/autoscaling/types" @@ -270,9 +271,24 @@ func (t *TargetPlugin) calculateDirection(asgDesired, strategyDesired int64) (in return 0, "" } +func warmPoolActivity(activity types.Activity) bool { + if activity.Description == nil { + return false + } + + desc := *activity.Description + return strings.Contains(desc, "Launching a new EC2 instance into warm pool:") || + strings.Contains(desc, "Terminating EC2 instance from warm pool:") +} + // processLastActivity updates the status object based on the details within // the last scaling activity. func processLastActivity(activity types.Activity, status *sdk.TargetStatus) { + // Warm pool activities are not considered scaling activities in the context of + // scaling the asg in or out, so we skip them. + if warmPoolActivity(activity) { + return + } // If the last activities progress is not nil then check whether this // finished or not. In the event there is a current activity in progress diff --git a/plugins/builtin/target/aws-asg/plugin/plugin_test.go b/plugins/builtin/target/aws-asg/plugin/plugin_test.go index c4963f56..f87282d4 100644 --- a/plugins/builtin/target/aws-asg/plugin/plugin_test.go +++ b/plugins/builtin/target/aws-asg/plugin/plugin_test.go @@ -55,6 +55,57 @@ func TestTargetPlugin_calculateDirection(t *testing.T) { } } +func Test_warmPoolActivity(t *testing.T) { + testCases := []struct { + inputActivity types.Activity + expectedResponse bool + name string + }{ + { + inputActivity: types.Activity{ + Description: ptr.Of("Launching a new EC2 instance into warm pool: i-0b22a22eec53b9321"), + }, + expectedResponse: true, + name: "instance launched into warm pool", + }, + { + inputActivity: types.Activity{ + Description: ptr.Of("Terminating EC2 instance from warm pool: i-0b22a22eec53b9321"), + }, + expectedResponse: true, + name: "instance terminated from warm pool", + }, + { + inputActivity: types.Activity{ + Description: ptr.Of("Launching a new EC2 instance from warm pool: i-0b22a22eec53b9321"), + }, + expectedResponse: false, + name: "instance launched into ASG from warm pool", + }, + { + inputActivity: types.Activity{ + Description: nil, + }, + expectedResponse: false, + name: "nil description", + }, + { + inputActivity: types.Activity{ + Description: ptr.Of(""), + }, + expectedResponse: false, + name: "empty description", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + res := warmPoolActivity(tc.inputActivity) + assert.Equal(t, tc.expectedResponse, res, tc.name) + }) + } +} + func Test_processLastActivity(t *testing.T) { testTime := time.Date(2020, time.April, 13, 8, 4, 0, 0, time.UTC) @@ -114,6 +165,40 @@ func Test_processLastActivity(t *testing.T) { }, name: "latest activity all nils", }, + { + inputActivity: types.Activity{ + Progress: ptr.Of(int32(75)), + Description: ptr.Of("Launching a new EC2 instance into warm pool:"), + }, + inputStatus: &sdk.TargetStatus{ + Ready: true, + Count: 1, + Meta: map[string]string{}, + }, + expectedStatus: &sdk.TargetStatus{ + Ready: true, + Count: 1, + Meta: map[string]string{}, + }, + name: "latest activity was instance launched into warm pool", + }, + { + inputActivity: types.Activity{ + Progress: ptr.Of(int32(75)), + Description: ptr.Of("Terminating EC2 instance from warm pool:"), + }, + inputStatus: &sdk.TargetStatus{ + Ready: true, + Count: 1, + Meta: map[string]string{}, + }, + expectedStatus: &sdk.TargetStatus{ + Ready: true, + Count: 1, + Meta: map[string]string{}, + }, + name: "latest activity was instance terminated from warm pool", + }, } for _, tc := range testCases {