UNPKG

@cloud-carbon-footprint/gcp

Version:

The core logic to get cloud usage data and estimate energy and carbon emissions from Google Cloud Platform.

697 lines (599 loc) 22.5 kB
/* * © 2021 Thoughtworks, Inc. */ import { AddressesClient, DisksClient, ImagesClient, InstancesClient, MachineTypesClient, } from '@google-cloud/compute' import { GoogleAuth } from 'google-auth-library' import { RecommenderClient } from '@google-cloud/recommender' import { ProjectsClient } from '@google-cloud/resource-manager' import { RecommendationResult, GoogleAuthClient, } from '@cloud-carbon-footprint/common' import { ComputeEstimator, StorageEstimator, } from '@cloud-carbon-footprint/core' import { GCP_CLOUD_CONSTANTS } from '../domain' import Recommendations from '../lib/Recommendations' import ServiceWrapper from '../lib/ServiceWrapper' import { mockedProjects } from './fixtures/resourceManager.fixtures' import { setupSpy, setupSpyWithMultipleValues } from './helpers' import { mockChangeMachineTypeRecommendationsResults, mockDeleteDiskRecommendationsResults, mockDeleteImageRecommendationsResults, mockDeleteAddressRecommendationsResults, mockEmptyRecommendationsResults, mockSnapshotAndDeleteDiskRecommendationsResults, mockStopVMRecommendationsResults, mockStopVMWithAdditionalImpactRecommendationsResults, mockStopVmAndDeleteAddressRecommendations, mockDeleteAddressRecommendationsEast, } from './fixtures/recommender.fixtures' import { mockAddressesResultItems, mockDisksResultItems, mockedInstanceGetItems, mockedInstanceGetItemsCurrent, mockedInstanceGetItemsWithBothDisks, mockedInstanceGetItemsWithHDDDisks, mockInstanceResultItems, mockedMachineTypesGetItems, mockedMachineTypesGetItemsNew, mockedMachineTypesGetItemsCurrent, mockedDisksGetSSDDetails, mockedDisksGetHDDDetails, mockedImageGetDetails, mockInstanceGlobalResultItems, mockInstanceRegionsResultItems, mockedAddressGetDetails, } from './fixtures/googleapis.fixtures' jest.mock('moment', () => { return () => jest.requireActual('moment')('2020-04-01T00:00:00.000Z') }) jest.mock('@google-cloud/resource-manager', () => ({ ProjectsClient: jest.fn().mockImplementation(() => ({ searchProjects: jest.fn().mockResolvedValue(mockedProjects), })), })) const mockListRecommendations = jest.fn() jest.mock('@google-cloud/recommender', () => ({ RecommenderClient: jest.fn().mockImplementation(() => ({ listRecommendations: mockListRecommendations, projectLocationRecommenderPath: jest.fn(), })), })) describe('GCP Recommendations Service', () => { let googleAuthClient: GoogleAuthClient, serviceWrapper: ServiceWrapper, mockGoogleCompute: any beforeEach(async () => { const googleAuth = new GoogleAuth({ scopes: ['https://www.googleapis.com/auth/cloud-platform'], }) const getClientSpy = jest.spyOn(googleAuth, 'getClient') ;(getClientSpy as jest.Mock).mockResolvedValue(jest.fn()) googleAuthClient = await googleAuth.getClient() mockGoogleCompute = { instances: new InstancesClient(), machineTypes: new MachineTypesClient(), disks: new DisksClient(), images: new ImagesClient(), addresses: new AddressesClient(), } setupSpy( mockGoogleCompute.instances, 'aggregatedListAsync', mockInstanceResultItems(), ) setupSpy( mockGoogleCompute.disks, 'aggregatedListAsync', mockDisksResultItems(), ) setupSpy( mockGoogleCompute.addresses, 'aggregatedListAsync', mockAddressesResultItems(), ) serviceWrapper = new ServiceWrapper( new ProjectsClient(), googleAuthClient, mockGoogleCompute.instances, mockGoogleCompute.disks, mockGoogleCompute.addresses, mockGoogleCompute.images, mockGoogleCompute.machineTypes, new RecommenderClient(), ) }) describe('Stop VM Recommendations', () => { beforeEach(() => { setupSpy( mockGoogleCompute.machineTypes, 'get', mockedMachineTypesGetItems, ) }) it('returns recommendations for stop VM with no storage', async () => { mockListRecommendations .mockResolvedValueOnce(mockStopVMRecommendationsResults) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.instances, 'get', mockedInstanceGetItems) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 56.26368, co2eSavings: 0.00444483072, costSavings: 15, instanceName: 'test-resource-name', resourceId: '12456789012', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for stop VM with additional impact', async () => { mockListRecommendations .mockResolvedValueOnce( mockStopVMWithAdditionalImpactRecommendationsResults, ) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.instances, 'get', mockedInstanceGetItems) setupSpy( mockGoogleCompute.instances, 'aggregatedListAsync', mockInstanceGlobalResultItems(), ) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'Unknown', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 56.26368, co2eSavings: 0.017445576958977024, costSavings: 55, instanceName: 'test-resource-name', resourceId: '12456789012', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for stop VM with active regions', async () => { mockListRecommendations .mockResolvedValueOnce(mockStopVMRecommendationsResults) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.instances, 'get', mockedInstanceGetItems) setupSpy( mockGoogleCompute.instances, 'aggregatedListAsync', mockInstanceRegionsResultItems(), ) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 56.26368, co2eSavings: 0.00444483072, costSavings: 15, instanceName: 'test-resource-name', resourceId: '12456789012', }, ] expect(recommendations).toEqual(expectedResult) }) it('does not return recommendations for stop VM with an error', async () => { mockListRecommendations .mockResolvedValueOnce(mockStopVMRecommendationsResults) .mockResolvedValue([[]]) const targetFunctionSpy = jest.spyOn(mockGoogleCompute.instances, 'get') ;(targetFunctionSpy as jest.Mock).mockRejectedValue('Error') const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 0, co2eSavings: 0, costSavings: 15, resourceId: '', instanceName: 'instance-name', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for stop VM with an HDD disk for storage', async () => { mockListRecommendations .mockResolvedValueOnce(mockStopVMRecommendationsResults) .mockResolvedValue([[]]) setupSpy( mockGoogleCompute.instances, 'get', mockedInstanceGetItemsWithHDDDisks, ) setupSpy(mockGoogleCompute.disks, 'get', mockedDisksGetHDDDetails) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 56.273976, co2eSavings: 0.004445644104, costSavings: 15, instanceName: 'test-instance-name', resourceId: '12456789012', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for stop VM with an HDD and SSD disks for storage', async () => { mockListRecommendations .mockResolvedValueOnce(mockStopVMRecommendationsResults) .mockResolvedValue([[]]) setupSpy( mockGoogleCompute.instances, 'get', mockedInstanceGetItemsWithBothDisks, ) setupSpyWithMultipleValues( mockGoogleCompute.disks, 'get', mockedDisksGetSSDDetails, mockedDisksGetHDDDetails, ) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 56.292984000000004, co2eSavings: 0.004447145736, costSavings: 15, instanceName: 'test-instance-name', resourceId: '12456789012', }, ] expect(recommendations).toEqual(expectedResult) }) }) it('returns recommendations for change machine type', async () => { mockListRecommendations .mockResolvedValueOnce(mockChangeMachineTypeRecommendationsResults) .mockResolvedValue([[]]) setupSpyWithMultipleValues( mockGoogleCompute.machineTypes, 'get', mockedMachineTypesGetItemsCurrent, mockedMachineTypesGetItemsNew, ) setupSpy(mockGoogleCompute.instances, 'get', mockedInstanceGetItemsCurrent) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'CHANGE_MACHINE_TYPE', recommendationDetail: 'Save cost by changing machine type from e2-medium to e2-small.', kilowattHourSavings: 1.89684, co2eSavings: 0.00014985036, costSavings: 20, resourceId: '12456789012', instanceName: 'test-resource-name', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for change machine type with failed getInstance call', async () => { mockListRecommendations .mockResolvedValueOnce(mockChangeMachineTypeRecommendationsResults) .mockResolvedValue([[]]) setupSpyWithMultipleValues( mockGoogleCompute.machineTypes, 'get', mockedMachineTypesGetItemsCurrent, mockedMachineTypesGetItemsNew, ) const targetFunctionSpy = jest.spyOn(mockGoogleCompute.instances, 'get') ;(targetFunctionSpy as jest.Mock).mockRejectedValue('Error') const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'CHANGE_MACHINE_TYPE', recommendationDetail: 'Save cost by changing machine type from e2-medium to e2-small.', kilowattHourSavings: 1.89684, co2eSavings: 0.00014985036, costSavings: 20, resourceId: '', instanceName: 'instance-name', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for delete SSD disk', async () => { mockListRecommendations .mockResolvedValueOnce(mockDeleteDiskRecommendationsResults) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.disks, 'get', mockedDisksGetSSDDetails) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'DELETE_DISK', recommendationDetail: "Save cost by deleting idle persistent disk 'test-disk'.", kilowattHourSavings: 0.019008000000000004, co2eSavings: 0.0000015016320000000002, costSavings: 50, resourceId: '12456789012', instanceName: 'test-resource-name', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for delete HDD disk', async () => { mockListRecommendations .mockResolvedValueOnce(mockSnapshotAndDeleteDiskRecommendationsResults) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.disks, 'get', mockedDisksGetHDDDetails) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'SNAPSHOT_AND_DELETE_DISK', recommendationDetail: "Save cost by deleting idle persistent disk 'test-disk'.", kilowattHourSavings: 0.010296000000000003, co2eSavings: 8.133840000000002e-7, costSavings: 50, resourceId: '12456789012', instanceName: 'test-resource-name', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns recommendations for delete image', async () => { mockListRecommendations .mockResolvedValueOnce(mockDeleteImageRecommendationsResults) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.images, 'get', mockedImageGetDetails) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'DELETE_IMAGE', recommendationDetail: "Save cost by deleting idle image 'test-image'.", kilowattHourSavings: 0.0002784182797193527, co2eSavings: 2.1995044097828865e-8, costSavings: 30, resourceId: '12456789012', instanceName: 'test-resource-name', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns estimates of zero for recommendation type DELETE_ADDRESS', async () => { mockListRecommendations .mockResolvedValueOnce(mockDeleteAddressRecommendationsResults) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.addresses, 'get', mockedAddressGetDetails) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'DELETE_ADDRESS', recommendationDetail: "Save cost by deleting idle address 'test-address'.", kilowattHourSavings: 0, co2eSavings: 0, costSavings: 40, resourceId: '123456789012345', instanceName: 'test-address', }, ] expect(recommendations).toEqual(expectedResult) }) it('returns estimates for recommendation type DELETE_ADDRESS', async () => { mockListRecommendations .mockResolvedValueOnce(mockStopVmAndDeleteAddressRecommendations) .mockResolvedValueOnce(mockDeleteAddressRecommendationsEast) .mockResolvedValue([[]]) setupSpy(mockGoogleCompute.machineTypes, 'get', mockedMachineTypesGetItems) setupSpy(mockGoogleCompute.instances, 'get', mockedInstanceGetItems) setupSpy(mockGoogleCompute.addresses, 'get', mockedAddressGetDetails) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [ { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'STOP_VM', recommendationDetail: "Save cost by stopping Idle VM 'test-instance'.", kilowattHourSavings: 56.26368, co2eSavings: 0.00444483072, costSavings: 15, instanceName: 'test-resource-name', resourceId: '12456789012', }, { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-west1', recommendationType: 'DELETE_ADDRESS', recommendationDetail: "Save cost by deleting idle address 'test-address'.", kilowattHourSavings: 150.03648, co2eSavings: 0.01185288192, costSavings: 40, resourceId: '123456789012345', instanceName: 'test-address', }, { cloudProvider: 'GCP', accountId: 'project', accountName: 'project-name', region: 'us-east1', recommendationType: 'DELETE_ADDRESS', recommendationDetail: "Save cost by deleting idle address 'test-address'.", kilowattHourSavings: 150.03648, co2eSavings: 0.08642101248, costSavings: 40, resourceId: '123456789012345', instanceName: 'test-address', }, ] expect(recommendations).toEqual(expectedResult) }) it('does not return recommendations with an error getting recommender ids ', async () => { mockListRecommendations .mockRejectedValue(mockEmptyRecommendationsResults) .mockResolvedValue([[]]) const recommendationsService = new Recommendations( new ComputeEstimator(), new StorageEstimator(GCP_CLOUD_CONSTANTS.HDDCOEFFICIENT!), new StorageEstimator(GCP_CLOUD_CONSTANTS.SSDCOEFFICIENT!), serviceWrapper, ) const recommendations = await recommendationsService.getRecommendations() const expectedResult: RecommendationResult[] = [] expect(recommendations).toEqual(expectedResult) }) })