@cloud-carbon-footprint/aws
Version:
The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.
346 lines (301 loc) • 11.1 kB
text/typescript
/*
* © 2021 Thoughtworks, Inc.
*/
import {
GetRightsizingRecommendationCommand,
GetRightsizingRecommendationCommandOutput,
} from '@aws-sdk/client-cost-explorer'
import { mockClient } from 'aws-sdk-client-mock'
import moment from 'moment'
import { CloudWatchClient } from '@aws-sdk/client-cloudwatch'
import { CostExplorerClient } from '@aws-sdk/client-cost-explorer'
import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs'
import { S3Client } from '@aws-sdk/client-s3'
import { ComputeEstimator, MemoryEstimator } from '@cloud-carbon-footprint/core'
import {
AWS_DEFAULT_RECOMMENDATION_TARGET,
AWS_RECOMMENDATIONS_TARGETS,
RecommendationResult,
} from '@cloud-carbon-footprint/common'
import {
rightsizingCrossFamilyRecommendationModify,
rightsizingCrossFamilyRecommendationTerminate,
rightsizingRecommendationModify,
rightsizingRecommendationModify1,
rightsizingRecommendationTerminate,
} from './fixtures/costExplorer.fixtures'
import { AWS_CLOUD_CONSTANTS } from '../domain'
import { ServiceWrapper, RightsizingRecommendations } from '../lib'
describe('AWS Rightsizing Recommendations Service', () => {
const getServiceWrapper = () =>
new ServiceWrapper(
new CloudWatchClient(),
new CloudWatchLogsClient(),
new CostExplorerClient(),
new S3Client(),
)
const costExplorerMock = mockClient(CostExplorerClient)
afterEach(() => {
costExplorerMock.reset()
jest.restoreAllMocks()
getRightsizingRecommendationSpy.mockClear()
})
const getRightsizingRecommendationSpy = jest.fn()
function mockGetRightsizingRecommendation(
response: GetRightsizingRecommendationCommandOutput,
) {
costExplorerMock.on(GetRightsizingRecommendationCommand).resolves(response)
}
it('Get recommendations from Rightsizing API type: Terminate with pagination', async () => {
moment.now = function () {
return +new Date('2020-04-01T00:00:00.000Z')
}
mockGetRightsizingRecommendation(rightsizingRecommendationTerminate)
const awsRecommendationsServices = new RightsizingRecommendations(
new ComputeEstimator(),
new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
getServiceWrapper(),
)
const result = await awsRecommendationsServices.getRecommendations(
AWS_DEFAULT_RECOMMENDATION_TARGET,
)
const calls = costExplorerMock.commandCalls(
GetRightsizingRecommendationCommand,
)
expect(calls).toHaveLength(1)
expect(calls[0].args[0].input).toEqual({
Service: 'AmazonEC2',
Configuration: {
BenefitsConsidered: false,
RecommendationTarget: 'SAME_INSTANCE_FAMILY',
},
})
const expectedResult: RecommendationResult[] = [
{
cloudProvider: 'AWS',
accountId: 'test-account',
accountName: 'test-account',
region: 'us-east-2',
recommendationType: 'TERMINATE',
recommendationDetail: 'TERMINATE instance: test-instance-name.',
kilowattHourSavings: 275.386397472,
resourceId: 'test-id',
instanceName: 'test-instance-name',
co2eSavings: 0.10358062264645465,
costSavings: 20,
},
{
cloudProvider: 'AWS',
accountId: 'test-account-1',
accountName: 'test-account-1',
region: 'us-east-2',
recommendationType: 'TERMINATE',
recommendationDetail: 'TERMINATE instance: test-instance-name.',
kilowattHourSavings: 62.1072,
resourceId: 'test-id',
instanceName: 'test-instance-name',
co2eSavings: 0.02336027670895392,
costSavings: 80,
},
{
cloudProvider: 'AWS',
accountId: 'test-account-2',
accountName: 'test-account-2',
region: 'us-east-2',
recommendationType: 'TERMINATE',
recommendationDetail: 'TERMINATE instance: test-instance-name.',
kilowattHourSavings: 0.38817,
resourceId: 'test-id',
instanceName: 'test-instance-name',
co2eSavings: 0.00014600172943096202,
costSavings: 20,
},
{
accountId: 'test-account-3',
accountName: 'test-account-3',
cloudProvider: 'AWS',
co2eSavings: 0.00014600172943096202,
costSavings: 30,
kilowattHourSavings: 0.38817,
recommendationDetail: 'TERMINATE instance with Resource ID: test-id.',
recommendationType: 'TERMINATE',
region: 'us-east-2',
resourceId: 'test-id',
instanceName: '',
},
]
expect(result).toEqual(expectedResult)
})
it('Get recommendations from Rightsizing API type: Modify with targetInstance Co2e', async () => {
moment.now = function () {
return +new Date('2020-04-01T00:00:00.000Z')
}
// Simulate AWS returning case type from runtime payload
const modifyResponse: GetRightsizingRecommendationCommandOutput =
JSON.parse(JSON.stringify(rightsizingRecommendationModify1))
modifyResponse.RightsizingRecommendations![0].RightsizingType =
'Modify' as unknown as any
mockGetRightsizingRecommendation(modifyResponse)
const awsRecommendationsServices = new RightsizingRecommendations(
new ComputeEstimator(),
new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
getServiceWrapper(),
)
const result = await awsRecommendationsServices.getRecommendations(
AWS_DEFAULT_RECOMMENDATION_TARGET,
)
const expectedResult: RecommendationResult[] = [
{
cloudProvider: 'AWS',
accountId: 'test-account',
accountName: 'test-account',
region: 'us-east-2',
recommendationType: 'Modify',
recommendationDetail:
'Modify instance: test-instance-name. Update instance type m5.xlarge to m5.large',
kilowattHourSavings: 2.0615368199999997,
resourceId: 'Test-resource-id',
instanceName: 'test-instance-name',
co2eSavings: 0.0007754023778385905,
costSavings: 226,
},
]
expect(result).toEqual(expectedResult)
})
it('Get recommendations from Rightsizing API type: Modify', async () => {
moment.now = function () {
return +new Date('2020-04-01T00:00:00.000Z')
}
mockGetRightsizingRecommendation(rightsizingRecommendationModify)
const awsRecommendationsServices = new RightsizingRecommendations(
new ComputeEstimator(),
new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
getServiceWrapper(),
)
const result = await awsRecommendationsServices.getRecommendations(
AWS_DEFAULT_RECOMMENDATION_TARGET,
)
const calls = costExplorerMock.commandCalls(
GetRightsizingRecommendationCommand,
)
expect(calls).toHaveLength(1)
expect(calls[0].args[0].input).toEqual({
Service: 'AmazonEC2',
Configuration: {
BenefitsConsidered: false,
RecommendationTarget: 'SAME_INSTANCE_FAMILY',
},
})
const expectedResult: RecommendationResult[] = [
{
cloudProvider: 'AWS',
accountId: 'test-account',
accountName: 'test-account',
region: 'us-east-2',
recommendationType: 'MODIFY',
recommendationDetail:
'MODIFY instance: test-instance-name. Update instance type t2.micro to t2.nano',
kilowattHourSavings: 0.194085,
resourceId: 'Test-resource-id',
instanceName: 'test-instance-name',
co2eSavings: 0.00007300086471548101,
costSavings: 226,
},
]
expect(result).toEqual(expectedResult)
})
it('Get recommendations from Rightsizing API type: Terminate with Cross Family parameter', async () => {
moment.now = function () {
return +new Date('2020-04-01T00:00:00.000Z')
}
mockGetRightsizingRecommendation(
rightsizingCrossFamilyRecommendationTerminate,
)
const awsRecommendationsServices = new RightsizingRecommendations(
new ComputeEstimator(),
new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
getServiceWrapper(),
)
const result = await awsRecommendationsServices.getRecommendations(
AWS_RECOMMENDATIONS_TARGETS.CROSS_INSTANCE_FAMILY,
)
const calls = costExplorerMock.commandCalls(
GetRightsizingRecommendationCommand,
)
expect(calls).toHaveLength(1)
expect(calls[0].args[0].input).toEqual({
Service: 'AmazonEC2',
Configuration: {
BenefitsConsidered: false,
RecommendationTarget: AWS_RECOMMENDATIONS_TARGETS.CROSS_INSTANCE_FAMILY,
},
})
const expectedResult: RecommendationResult[] = [
{
cloudProvider: 'AWS',
accountId: 'test-account',
accountName: 'test-account',
region: 'us-east-2',
recommendationType: 'TERMINATE',
recommendationDetail: 'TERMINATE instance: test-instance-name.',
kilowattHourSavings: 0.38817,
resourceId: 'Test-resource-id',
instanceName: 'test-instance-name',
co2eSavings: 0.00014600172943096202,
costSavings: 20,
},
]
expect(result).toEqual(expectedResult)
})
it('Get recommendations from Rightsizing API type: Modify with Cross Family parameter', async () => {
moment.now = function () {
return +new Date('2020-04-01T00:00:00.000Z')
}
mockGetRightsizingRecommendation(rightsizingCrossFamilyRecommendationModify)
const awsRecommendationsServices = new RightsizingRecommendations(
new ComputeEstimator(),
new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
getServiceWrapper(),
)
const result = await awsRecommendationsServices.getRecommendations(
AWS_RECOMMENDATIONS_TARGETS.CROSS_INSTANCE_FAMILY,
)
const expectedResult: RecommendationResult[] = [
{
cloudProvider: 'AWS',
accountId: 'test-account',
accountName: 'test-account',
region: 'us-east-2',
recommendationType: 'MODIFY',
recommendationDetail:
'MODIFY instance: test-instance-name. Update instance type t2.micro to t3.micro',
kilowattHourSavings: -0.38817,
resourceId: 'Test-resource-id',
instanceName: 'test-instance-name',
co2eSavings: -0.00014600172943096202,
costSavings: 20,
},
]
expect(result).toEqual(expectedResult)
})
it('Logs the error response if there is a problem getting recommendations', async () => {
getRightsizingRecommendationSpy.mockRejectedValue({
message: 'error-test',
})
costExplorerMock
.on(GetRightsizingRecommendationCommand)
.callsFake(getRightsizingRecommendationSpy)
const awsRecommendationsServices = new RightsizingRecommendations(
new ComputeEstimator(),
new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
getServiceWrapper(),
)
await expect(() =>
awsRecommendationsServices.getRecommendations(
AWS_DEFAULT_RECOMMENDATION_TARGET,
),
).rejects.toThrow(
`Failed to grab AWS Rightsizing recommendations. Reason: error-test`,
)
})
})