Skip to content
This repository was archived by the owner on Jul 1, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/instance_terminator.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function terminateOldestInstanceFromEachGrouped(autoscalingGroups) {
instanceTerminatorGroupNames.forEach(function(instanceTerminatorGroupName) {
promises.push(new Promise(function(resolve) {

const matchingAutoscalingGroups = autoscalingGroups.filter(item => containsTag(item['Tags'], 'instance-terminator-group', instanceTerminatorGroupName));
const matchingAutoscalingGroups = autoscalingGroups.filter(item => containsTagValue(item['Tags'], 'instance-terminator-group', instanceTerminatorGroupName));
console.log('Attempting to terminate instance from: ' + instanceTerminatorGroupName);

if (matchingAutoscalingGroups.length < 2) {
Expand Down Expand Up @@ -118,7 +118,7 @@ function terminateOldestInstanceFrom(matchingAutoscalingGroups) {
const instances = autoscalingGroup['Instances'];
const healthyInstances = instances.filter(instance => instance['LifecycleState'] == 'InService' && instance['HealthStatus'] == 'Healthy');

if (healthyInstances.length < desiredCapacity) {
if (healthyInstances.length < desiredCapacity || desiredCapacity < 1) {
console.log('Too few healthy instances, ignoring.')
var response = {
result: 'not enough healthy instances in group'
Expand Down Expand Up @@ -177,4 +177,11 @@ function containsTag(tags, tagKey) {
if (tag['Key'] == tagKey)
return true;
})
}

function containsTagValue(tags, tagKey, tagValue) {
return tags.some(function (tag) {
if (tag['Key'] == tagKey && tag['Value'] == tagValue)
return true;
})
}
193 changes: 193 additions & 0 deletions test/unit/instance_terminator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,114 @@ describe( 'instance-terminator', function() {
expect(terminateInstance.notCalled, 'terminate instance should not be called').to.be.true;
});
});

it( `should not terminate an instance across grouped asgs if the group has no instances`, function() {
const terminateInstance = sinon.spy();
const lookupOldestInstance = sinon.spy();
const myLambda = proxyquire( '../../src/instance_terminator', {
'./aws/autoscaling_handler': {
findAutoscalingGroupsByTag: (tagKey, tagValue) => {
expect(tagKey).to.equal('can-be-terminated');
expect(tagValue).to.equal('true');
return new Promise(function (resolve) {
resolve(describeAutoscalingGroupsResponse_withGroupedAsgsWithNoInstances);
});
},
terminateInstance: terminateInstance
},
'./aws/ec2_handler': {
lookupOldestInstance: lookupOldestInstance
},
});

return LambdaTester(myLambda.handler)
.event()
.expectResult((result) => {
expect(result).to.have.lengthOf(1);
expect(result).to.have.deep.members([{instanceTerminatorGroupName: 'my-test-group', result: 'not enough healthy instances in group'}]);
expect(lookupOldestInstance.notCalled, 'lookupOldestInstance should not be called').to.be.true;
expect(terminateInstance.notCalled, 'terminate instance should not be called').to.be.true;
});
});

it( `should terminate one instance when there is more than one grouped asgs`, function() {
const terminateInstance = sinon.spy();
const myLambda = proxyquire( '../../src/instance_terminator', {
'./aws/autoscaling_handler': {
findAutoscalingGroupsByTag: (tagKey, tagValue) => {
expect(tagKey).to.equal('can-be-terminated');
expect(tagValue).to.equal('true');
return new Promise(function (resolve) {
resolve(describeAutoscalingGroupsResponse_withMultpleGroupedAsgs);
});
},
terminateInstance: terminateInstance
},
'./aws/ec2_handler': {
lookupOldestInstance: (instances) => {
if (instances[0]['InstanceId'] == 'i-grouped-asgs-1') {
expect(instances).to.deep.equal([{
InstanceId: 'i-grouped-asgs-1',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}, {
InstanceId: 'i-grouped-asgs-2',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}]);
return new Promise(function (resolve) {
resolve({InstanceId: 'i-grouped-asgs-2',LaunchTime: new Date('2018-01-11T09:56:50.000Z')});
});
}
if (instances[0]['InstanceId'] == 'i-grouped-asgs-3') {
expect(instances).to.deep.equal([{
InstanceId: 'i-grouped-asgs-3',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}, {
InstanceId: 'i-grouped-asgs-4',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}]);
return new Promise(function (resolve) {
resolve({InstanceId: 'i-grouped-asgs-4',LaunchTime: new Date('2018-01-11T09:56:50.000Z')});
});
}
if (instances[0]['InstanceId'] == 'i-grouped-asgs-2') {
expect(instances).to.deep.equal([{
InstanceId: 'i-grouped-asgs-2',
LaunchTime: new Date('2018-01-11T09:56:50.000Z')
}, {
InstanceId: 'i-grouped-asgs-4',
LaunchTime: new Date('2018-01-11T09:56:50.000Z')
}]);
return new Promise(function (resolve) {
resolve({InstanceId: 'i-grouped-asgs-4', LaunchTime: new Date('2018-01-11T09:56:50.000Z')});
});
}
}
},
});

return LambdaTester(myLambda.handler)
.event()
.expectResult((result) => {
expect(result).to.have.lengthOf(2);
expect(result).to.have.deep.members([
{
instanceTerminatorGroupName: "my-test-group1",
result: 'instance terminated',
instanceId: 'i-grouped-asgs-4'
},{
instanceTerminatorGroupName: "my-test-group2",
result: 'instance-terminator-group tag only attached to one autoscaling group'
}
]);

expect(terminateInstance.callCount, 'terminate instance called once').to.equal(1);
expect(terminateInstance.calledWith('i-grouped-asgs-4'), 'terminate instance parameters').to.be.true;
});
});
});

const describeAutoscalingGroupsResponse_withTooFewInstances = [
Expand Down Expand Up @@ -451,4 +559,89 @@ const describeAutoscalingGroupsResponse_withBadlyGroupedAsgs = [
Value: 'my-test-group'
}]
}
]

const describeAutoscalingGroupsResponse_withGroupedAsgsWithNoInstances = [
{
AutoScalingGroupName: 'my-asg-grouped-asgs',
MinSize: 0,
MaxSize: 0,
DesiredCapacity: 0,
Instances: [],
Tags: [{
Key: 'instance-terminator-group',
Value: 'my-test-group'
}]
},
{
AutoScalingGroupName: 'another-asg-grouped-asgs',
MinSize: 0,
MaxSize: 0,
DesiredCapacity: 0,
Instances: [],
Tags: [{
Key: 'instance-terminator-group',
Value: 'my-test-group'
}]
}
]

const describeAutoscalingGroupsResponse_withMultpleGroupedAsgs = [
{
AutoScalingGroupName: 'my-asg-grouped-asgs',
MinSize: 2,
MaxSize: 2,
DesiredCapacity: 2,
Instances: [{
InstanceId: 'i-grouped-asgs-1',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}, {
InstanceId: 'i-grouped-asgs-2',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}],
Tags: [{
Key: 'instance-terminator-group',
Value: 'my-test-group1'
}]
},
{
AutoScalingGroupName: 'another-asg-grouped-asgs',
MinSize: 2,
MaxSize: 2,
DesiredCapacity: 2,
Instances: [{
InstanceId: 'i-grouped-asgs-3',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}, {
InstanceId: 'i-grouped-asgs-4',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}],
Tags: [{
Key: 'instance-terminator-group',
Value: 'my-test-group1'
}]
},
{
AutoScalingGroupName: 'third-grouped-asgs',
MinSize: 2,
MaxSize: 2,
DesiredCapacity: 2,
Instances: [{
InstanceId: 'i-grouped-asgs-5',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}, {
InstanceId: 'i-grouped-asgs-6',
LifecycleState: 'InService',
HealthStatus: 'Healthy'
}],
Tags: [{
Key: 'instance-terminator-group',
Value: 'my-test-group2'
}]
},
]