UNPKG

@cloud-carbon-footprint/aws

Version:

The core logic to get cloud usage data and estimate energy and carbon emissions from Amazon Web Services.

623 lines (553 loc) 18.5 kB
/* * © 2021 Thoughtworks, Inc. */ import { Credentials } from 'aws-sdk' import { ServiceConfigurationOptions } from 'aws-sdk/lib/service' import { CloudProviderAccount } from '@cloud-carbon-footprint/core' import { AWS_DEFAULT_RECOMMENDATION_TARGET, AWS_RECOMMENDATIONS_SERVICES, AWS_RECOMMENDATIONS_TARGETS, setConfig, EstimationResult, getPeriodEndDate, GroupBy, LookupTableInput, LookupTableOutput, RecommendationResult, } from '@cloud-carbon-footprint/common' import { CostAndUsageReports, EBS, EC2, ElastiCache, Lambda, RDS, S3, } from '../lib' import AWSCredentialsProvider from '../application/AWSCredentialsProvider' import { RightsizingRecommendations, ComputeOptimizerRecommendations, } from '../lib/Recommendations' jest.mock('../application/AWSCredentialsProvider') /* eslint-disable @typescript-eslint/no-var-requires */ describe('AWSAccount', () => { const CloudWatch = jest.fn() const CostExplorer = jest.fn() const CloudWatchLogs = jest.fn() const Athena = jest.fn() const S3Service = jest.fn() const GlueService = jest.fn() let expectedCredentials: Credentials beforeEach(() => { jest.doMock('aws-sdk', () => { return { CloudWatch: CloudWatch, CostExplorer: CostExplorer, CloudWatchLogs: CloudWatchLogs, Athena: Athena, S3: S3Service, Glue: GlueService, } }) const mockedCreate = jest.fn() expectedCredentials = new Credentials('test', 'test', 'test') mockedCreate.mockReturnValue(expectedCredentials) AWSCredentialsProvider.create = mockedCreate }) afterEach(() => { jest.restoreAllMocks() }) it('should return empty if no service in config file', () => { setConfig({ AWS: { CURRENT_SERVICES: [], }, }) const AWSAccount = require('../application/AWSAccount').default const services = new AWSAccount().getServices() expect(services).toHaveLength(0) }) it('should throw error if unknown service', () => { setConfig({ AWS: { CURRENT_SERVICES: [{ key: 'duck', name: '' }], }, }) const awsAccount = require('../application/AWSAccount').default const account = new awsAccount('123', 'us-east-1') expect(() => { account.getServices() }).toThrowError('Unsupported service: duck') }) it('should return instances from registered services in configuration file', () => { expectAWSService('ebs').toBeInstanceOf(EBS) expect(CloudWatch).toHaveBeenCalledWith({ region: 'some-region', credentials: expectedCredentials, }) expect(CostExplorer).toHaveBeenCalledWith({ region: 'us-east-1', credentials: expectedCredentials, }) }) it('should return s3 instance', () => { expectAWSService('s3').toBeInstanceOf(S3) }) it('should return ec2 instance', () => { expectAWSService('ec2').toBeInstanceOf(EC2) }) it('should return elasticache instance', () => { expectAWSService('elasticache').toBeInstanceOf(ElastiCache) }) it('should return rds instance', () => { expectAWSService('rds').toBeInstanceOf(RDS) }) it('should return lambda instance', () => { expectAWSService('lambda').toBeInstanceOf(Lambda) }) describe('credentials provider', () => { it('should set credentials to cloudwatch client', () => { // when expectAWSService('ebs') //then const options: ServiceConfigurationOptions = CloudWatch.mock.calls[0][0] expect(options.credentials).toEqual(expectedCredentials) }) it('should set credentials to costExplorer client', () => { // when expectAWSService('s3') //then const options: ServiceConfigurationOptions = CostExplorer.mock.calls[0][0] expect(options.credentials).toEqual(expectedCredentials) }) it('should set credentials to CloudWatchLogs client', () => { // when expectAWSService('lambda') //then const options: ServiceConfigurationOptions = CloudWatchLogs.mock.calls[0][0] expect(options.credentials).toEqual(expectedCredentials) }) }) it('should get data for regions', async () => { const startDate = new Date('2021-01-01') const endDate = new Date('2021-02-01') const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'region-a', ]) const expectedEstimatesResult: EstimationResult[] = getExpectedEstimationResult(startDate) const getRegionDataSpy = jest.spyOn( CloudProviderAccount.prototype, 'getRegionData', ) getRegionDataSpy.mockResolvedValue(expectedEstimatesResult) const result = await testAwsAccount.getDataForRegions( startDate, endDate, GroupBy.day, ) expect(result).toEqual(expectedEstimatesResult) }) it('should getDataFromCostAndUsageReports', async () => { const startDate = new Date('2021-01-01') const endDate = new Date('2021-02-01') const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'region-a', ]) const expectedEstimatesResult: EstimationResult[] = getExpectedEstimationResult(startDate) const costAndUsageReportsGetEstimatesSpy = jest.spyOn( CostAndUsageReports.prototype, 'getEstimates', ) costAndUsageReportsGetEstimatesSpy.mockResolvedValue( expectedEstimatesResult, ) const result = await testAwsAccount.getDataFromCostAndUsageReports( startDate, endDate, ) expect(result).toEqual(expectedEstimatesResult) }) it('should getCostAndUsageReportsDataFromInputData', async () => { const inputData: LookupTableInput[] = [ { serviceName: 'AmazonEC2', region: 'us-east-1', usageType: 'USE2-BoxUsage:t2.micro', usageUnit: 'Hrs', vCpus: '2', }, ] const AWSAccount = require('../application/AWSAccount').default const result = await AWSAccount.getCostAndUsageReportsDataFromInputData( inputData, ) const expectedResult: LookupTableOutput[] = [ { serviceName: 'AmazonEC2', region: 'us-east-1', usageType: 'USE2-BoxUsage:t2.micro', vCpus: '2', kilowattHours: 0.014425250356994983, co2e: 0.000005468165227575731, }, ] expect(result).toEqual(expectedResult) }) describe('Recommendations', () => { it('should get data for rightsizing recommendations', async () => { const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'some-region', ]) const expectedRecommendations: RecommendationResult[] = [ { cloudProvider: 'AWS', accountId: 'account-id', accountName: 'account-name', region: 'us-east-1', recommendationType: 'Terminate', recommendationDetail: 'Terminate instance: instance-name', kilowattHourSavings: 5, co2eSavings: 4, costSavings: 3, }, ] const getRecommendations = jest.spyOn( RightsizingRecommendations.prototype, 'getRecommendations', ) getRecommendations.mockResolvedValue(expectedRecommendations) const result = await testAwsAccount.getDataForRecommendations( AWS_DEFAULT_RECOMMENDATION_TARGET, ) expect(result).toEqual(expectedRecommendations) }) it('should get data for Cross Instance Family recommendations', async () => { const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'some-region', ]) const expectedRecommendations: RecommendationResult[] = [ { cloudProvider: 'AWS', accountId: 'account-id', accountName: 'account-name', region: 'us-east-1', recommendationType: 'Terminate', recommendationDetail: 'Terminate instance: instance-name', kilowattHourSavings: 5, co2eSavings: 4, costSavings: 3, }, ] const getRecommendations = jest.spyOn( RightsizingRecommendations.prototype, 'getRecommendations', ) getRecommendations.mockResolvedValue(expectedRecommendations) const result = await testAwsAccount.getDataForRecommendations( AWS_RECOMMENDATIONS_TARGETS.CROSS_INSTANCE_FAMILY, ) expect(result).toEqual(expectedRecommendations) expect(getRecommendations).toHaveBeenCalledWith( AWS_RECOMMENDATIONS_TARGETS.CROSS_INSTANCE_FAMILY, ) }) it('should get data for compute optimizer recommendations', async () => { const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'some-region', ]) setConfig({ AWS: { RECOMMENDATIONS_SERVICE: AWS_RECOMMENDATIONS_SERVICES.ComputeOptimizer, }, }) const expectedRecommendations: RecommendationResult[] = [ { cloudProvider: 'AWS', accountId: '1234567890', accountName: '1234567890', region: 'eu-central-1', recommendationType: 'EC2-OVER_PROVISIONED', kilowattHourSavings: 0, resourceId: 'i-0c80d1b0f3a0c5c69', instanceName: 'PA-VM-100 | Networks', co2eSavings: 0, recommendationDetail: 't3.xlarge', costSavings: 33.79, }, ] const getRecommendations = jest.spyOn( ComputeOptimizerRecommendations.prototype, 'getRecommendations', ) getRecommendations.mockResolvedValue(expectedRecommendations) const result = await testAwsAccount.getDataForRecommendations( AWS_DEFAULT_RECOMMENDATION_TARGET, ) expect(result).toEqual(expectedRecommendations) }) it('should get data for all recommendation services (compute optimizer and rightsizing)', async () => { const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'some-region', ]) setConfig({ AWS: { RECOMMENDATIONS_SERVICE: AWS_RECOMMENDATIONS_SERVICES.All, }, }) const expectedRecommendations: RecommendationResult[] = [ { cloudProvider: 'AWS', accountId: '1234567890', accountName: '1234567890', region: 'eu-central-1', recommendationType: 'EC2-OVER_PROVISIONED', kilowattHourSavings: 0, resourceId: 'i-0c80d1b0f3a0c5c69', instanceName: 'PA-VM-100 | Networks', co2eSavings: 0, recommendationDetail: 't3.xlarge', costSavings: 33.79, }, { cloudProvider: 'AWS', accountId: 'account-id', accountName: 'account-name', region: 'us-east-1', recommendationType: 'Terminate', recommendationDetail: 'Terminate instance: instance-name', kilowattHourSavings: 5, co2eSavings: 4, costSavings: 3, }, ] const getComputeOptimizerRecommendations = jest.spyOn( ComputeOptimizerRecommendations.prototype, 'getRecommendations', ) getComputeOptimizerRecommendations.mockResolvedValue([ expectedRecommendations[0], ]) const getRightsizingRecommendations = jest.spyOn( RightsizingRecommendations.prototype, 'getRecommendations', ) getRightsizingRecommendations.mockResolvedValue([ expectedRecommendations[1], ]) const result = await testAwsAccount.getDataForRecommendations( AWS_DEFAULT_RECOMMENDATION_TARGET, ) expect(result).toEqual(expectedRecommendations) }) it('should get data with highest savings when retrieving duplicate ids from all recommendation services', async () => { const AWSAccount = require('../application/AWSAccount').default const testAwsAccount = new AWSAccount('12345678', 'test account', [ 'some-region', ]) setConfig({ AWS: { RECOMMENDATIONS_SERVICE: AWS_RECOMMENDATIONS_SERVICES.All, }, }) const mockComputeOptimizerRecommendations = [ { cloudProvider: 'AWS', accountId: '1234567890', accountName: '1234567890', region: 'eu-central-1', recommendationType: 'EC2-OVER_PROVISIONED', kilowattHourSavings: 0, resourceId: 'i-0c80d1b0f3a0c5c69', instanceName: 'PA-VM-100 | Networks', co2eSavings: 3, recommendationDetail: 't3.xlarge', costSavings: 33.79, }, { cloudProvider: 'AWS', accountId: '0987654321', accountName: '0987654321', region: 'us-east-1', recommendationType: 'EC2-OVER_PROVISIONED', kilowattHourSavings: 0, resourceId: 'i-0c40d2b0p5a0c4c72', instanceName: 'PA-VM-100 | Networks', co2eSavings: 6, recommendationDetail: 't3.xlarge', costSavings: 33.79, }, { cloudProvider: 'AWS', accountId: '0987654321', accountName: '0987654321', region: 'us-east-1', recommendationType: 'EC2-OVER_PROVISIONED', kilowattHourSavings: 0, resourceId: 'i-0c90f1b0p8a0c4c47', instanceName: 'PA-VM-100 | Networks', co2eSavings: 8, recommendationDetail: 't3.xlarge', costSavings: 44.83, }, ] const mockRightsizingRecommendations: RecommendationResult[] = [ { cloudProvider: 'AWS', accountId: '1234567890', accountName: '1234567890', region: 'eu-central-1', recommendationType: 'Terminate', resourceId: 'i-0c80d1b0f3a0c5c69', recommendationDetail: 'Terminate instance: instance-name', kilowattHourSavings: 5, co2eSavings: 2, costSavings: 3, }, { cloudProvider: 'AWS', accountId: '0987654321', accountName: '0987654321', region: 'us-east-1', recommendationType: 'Modify', resourceId: 'i-0c40d2b0p5a0c4c72', recommendationDetail: 'Modify instance: instance-name', kilowattHourSavings: 5, co2eSavings: 6.12, costSavings: 3, }, { cloudProvider: 'AWS', accountId: '0987654321', accountName: '0987654321', region: 'us-east-1', recommendationType: 'Modify', kilowattHourSavings: 0, resourceId: 'i-0c90f1b0p8a0c4c47', recommendationDetail: 't3.xlarge', co2eSavings: 8, costSavings: 45.72, }, ] const expectedRecommendations = [ { cloudProvider: 'AWS', accountId: '1234567890', accountName: '1234567890', region: 'eu-central-1', recommendationType: 'EC2-OVER_PROVISIONED', kilowattHourSavings: 0, resourceId: 'i-0c80d1b0f3a0c5c69', instanceName: 'PA-VM-100 | Networks', co2eSavings: 3, recommendationDetail: 't3.xlarge', costSavings: 33.79, }, { cloudProvider: 'AWS', accountId: '0987654321', accountName: '0987654321', region: 'us-east-1', recommendationType: 'Modify', resourceId: 'i-0c40d2b0p5a0c4c72', recommendationDetail: 'Modify instance: instance-name', kilowattHourSavings: 5, co2eSavings: 6.12, costSavings: 3, }, { cloudProvider: 'AWS', accountId: '0987654321', accountName: '0987654321', region: 'us-east-1', recommendationType: 'Modify', kilowattHourSavings: 0, resourceId: 'i-0c90f1b0p8a0c4c47', recommendationDetail: 't3.xlarge', co2eSavings: 8, costSavings: 45.72, }, ] const getComputeOptimizerRecommendations = jest.spyOn( ComputeOptimizerRecommendations.prototype, 'getRecommendations', ) getComputeOptimizerRecommendations.mockResolvedValue( mockComputeOptimizerRecommendations, ) const getRightsizingRecommendations = jest.spyOn( RightsizingRecommendations.prototype, 'getRecommendations', ) getRightsizingRecommendations.mockResolvedValue( mockRightsizingRecommendations, ) const result = await testAwsAccount.getDataForRecommendations( AWS_DEFAULT_RECOMMENDATION_TARGET, ) expect(result).toEqual(expectedRecommendations) }) }) }) function expectAWSService(key: string) { setConfig({ AWS: { CURRENT_SERVICES: [{ key: key, name: '' }], ATHENA_REGION: 'us-east-1', IS_AWS_GLOBAL: true, }, }) const testRegion = 'some-region' const AWSAccount = require('../application/AWSAccount').default const services = new AWSAccount('12345678', 'test account', [ testRegion, ]).getServices(testRegion) return expect(services[0]) } function getExpectedEstimationResult(startDate: Date = new Date()) { const testAccountId = 'test account' const region = 'region-a' return [ { timestamp: new Date(startDate), serviceEstimates: [ { cloudProvider: 'AWS', accountId: testAccountId, accountName: testAccountId, serviceName: 'serviceOne', kilowattHours: 2, co2e: 2, cost: 3, region: region, usesAverageCPUConstant: false, }, { cloudProvider: 'AWS', accountId: testAccountId, accountName: testAccountId, serviceName: 'serviceTwo', kilowattHours: 1, co2e: 1, cost: 4, region: region, usesAverageCPUConstant: false, }, ], periodStartDate: new Date(startDate), periodEndDate: getPeriodEndDate(new Date(startDate), GroupBy.day), groupBy: GroupBy.day, }, ] }