UNPKG

@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
/* * © 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`, ) }) })