Skip to content

Commit 5d54a0c

Browse files
committed
Skip unauthorized AWS regions in multi-region fetches for AWS block actions
1 parent ed9ced3 commit 5d54a0c

18 files changed

Lines changed: 743 additions & 58 deletions

packages/blocks/aws-compute-optimizer/src/lib/common/compute-optimizer-client.ts

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ import {
33
GetRecommendationSummariesCommand,
44
RecommendationSummary,
55
} from '@aws-sdk/client-compute-optimizer';
6-
import { getAwsClient, makeAwsRequest } from '@openops/common';
6+
import {
7+
getAwsClient,
8+
isAwsPermissionError,
9+
makeAwsRequest,
10+
} from '@openops/common';
11+
import { logger } from '@openops/server-shared';
712

813
export async function getRecommendationSummaries(
914
credentials: any,
@@ -12,21 +17,33 @@ export async function getRecommendationSummaries(
1217
const results: RecommendationSummary[] = [];
1318

1419
for (const region of regions) {
15-
const client = getComputeOptimizerClient(credentials, region);
16-
const command = new GetRecommendationSummariesCommand({
17-
nextToken: '',
18-
});
19-
const regionalResults = await makeAwsRequest(client, command);
20+
try {
21+
const client = getComputeOptimizerClient(credentials, region);
22+
const command = new GetRecommendationSummariesCommand({
23+
nextToken: '',
24+
});
25+
const regionalResults = await makeAwsRequest(client, command);
2026

21-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
22-
for (const result of regionalResults as any) {
23-
const recommendationSummaries = result.recommendationSummaries?.map(
24-
(item: any) => ({ ...item, region }),
25-
);
27+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
28+
for (const result of regionalResults as any) {
29+
const recommendationSummaries = result.recommendationSummaries?.map(
30+
(item: any) => ({ ...item, region }),
31+
);
2632

27-
if (recommendationSummaries) {
28-
results.push(...recommendationSummaries);
33+
if (recommendationSummaries) {
34+
results.push(...recommendationSummaries);
35+
}
36+
}
37+
} catch (error) {
38+
if (isAwsPermissionError(error)) {
39+
logger.debug('Skipping AWS region due to permission error', {
40+
region,
41+
error,
42+
});
43+
continue;
2944
}
45+
46+
throw error;
3047
}
3148
}
3249

packages/blocks/aws-compute-optimizer/src/lib/common/compute-optimizer-ebs-client.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { EBSFinding } from '@aws-sdk/client-compute-optimizer';
2-
import { groupARNsByRegion } from '@openops/common';
2+
import { groupARNsByRegion, isAwsPermissionError } from '@openops/common';
3+
import { logger } from '@openops/server-shared';
34
import { EbsRecommendationsBuilder } from './ebs-recommendations-builder';
45
import { ComputeOptimizerRecommendation } from './get-recommendations';
56

@@ -18,12 +19,24 @@ export async function getEbsRecommendationsForRegions(
1819
);
1920

2021
for (const region of regions) {
21-
const recommendations = await recommendationsBuilder.getRecommendations(
22-
credentials,
23-
region,
24-
);
22+
try {
23+
const recommendations = await recommendationsBuilder.getRecommendations(
24+
credentials,
25+
region,
26+
);
2527

26-
result.push(...recommendations);
28+
result.push(...recommendations);
29+
} catch (error) {
30+
if (isAwsPermissionError(error)) {
31+
logger.debug('Skipping AWS region due to permission error', {
32+
region,
33+
error,
34+
});
35+
continue;
36+
}
37+
38+
throw error;
39+
}
2740
}
2841

2942
return result;
@@ -45,13 +58,25 @@ export async function getEbsRecommendationsForARNs(
4558
);
4659

4760
for (const region in arnsPerRegion) {
48-
const recommendations = await recommendationsBuilder.getRecommendations(
49-
credentials,
50-
region,
51-
arnsPerRegion[region],
52-
);
61+
try {
62+
const recommendations = await recommendationsBuilder.getRecommendations(
63+
credentials,
64+
region,
65+
arnsPerRegion[region],
66+
);
67+
68+
result.push(...recommendations);
69+
} catch (error) {
70+
if (isAwsPermissionError(error)) {
71+
logger.debug('Skipping AWS region due to permission error', {
72+
region,
73+
error,
74+
});
75+
continue;
76+
}
5377

54-
result.push(...recommendations);
78+
throw error;
79+
}
5580
}
5681

5782
return result;

packages/blocks/aws-compute-optimizer/src/lib/common/compute-optimizer-ec2-client.ts

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Finding } from '@aws-sdk/client-compute-optimizer';
2-
import { groupARNsByRegion } from '@openops/common';
2+
import { groupARNsByRegion, isAwsPermissionError } from '@openops/common';
3+
import { logger } from '@openops/server-shared';
34
import { Ec2RecommendationsBuilder } from './ec2-recommendations-builder';
45
import { ComputeOptimizerRecommendation } from './get-recommendations';
56

@@ -18,12 +19,24 @@ export async function getEC2RecommendationsForRegions(
1819
);
1920

2021
for (const region of regions) {
21-
const recommendations = await recommendationsBuilder.getRecommendations(
22-
credentials,
23-
region,
24-
);
22+
try {
23+
const recommendations = await recommendationsBuilder.getRecommendations(
24+
credentials,
25+
region,
26+
);
2527

26-
result.push(...recommendations);
28+
result.push(...recommendations);
29+
} catch (error) {
30+
if (isAwsPermissionError(error)) {
31+
logger.debug('Skipping AWS region due to permission error', {
32+
region,
33+
error,
34+
});
35+
continue;
36+
}
37+
38+
throw error;
39+
}
2740
}
2841

2942
return result;
@@ -46,13 +59,25 @@ export async function getEC2RecommendationsForARNs(
4659
);
4760

4861
for (const region in arnsPerRegion) {
49-
const recommendations = await recommendationsBuilder.getRecommendations(
50-
credentials,
51-
region,
52-
arnsPerRegion[region],
53-
);
62+
try {
63+
const recommendations = await recommendationsBuilder.getRecommendations(
64+
credentials,
65+
region,
66+
arnsPerRegion[region],
67+
);
68+
69+
result.push(...recommendations);
70+
} catch (error) {
71+
if (isAwsPermissionError(error)) {
72+
logger.debug('Skipping AWS region due to permission error', {
73+
region,
74+
error,
75+
});
76+
continue;
77+
}
5478

55-
result.push(...recommendations);
79+
throw error;
80+
}
5681
}
5782

5883
return result;

packages/blocks/aws-compute-optimizer/test/compute-optimizer-client.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
const debugMock = jest.fn();
2+
3+
jest.mock('@openops/server-shared', () => {
4+
const actual = jest.requireActual('@openops/server-shared');
5+
6+
return {
7+
...actual,
8+
logger: {
9+
...actual.logger,
10+
debug: debugMock,
11+
},
12+
};
13+
});
14+
115
const CREDENTIALS = {
216
accessKeyId: 'some accessKeyId',
317
secretAccessKey: 'some secretAccessKey',
@@ -160,6 +174,41 @@ describe('Get recommendations summary', () => {
160174
]);
161175
expect(openopsCommonMock.getAwsClient).toHaveBeenCalledTimes(2);
162176
});
177+
178+
test('should skip permission denied regions when fetching recommendation summaries', async () => {
179+
const summary = createRecommendationsSummaryResponse([
180+
{
181+
recommendationResourceType: RecommendationSourceType.EBS_VOLUME,
182+
},
183+
]);
184+
185+
openopsCommonMock.makeAwsRequest
186+
.mockRejectedValueOnce(new Error('AccessDenied'))
187+
.mockResolvedValueOnce([summary]);
188+
189+
const recommendations: any[] = await getRecommendationSummaries(
190+
CREDENTIALS,
191+
['region1', 'region2'],
192+
);
193+
194+
expect(recommendations).toHaveLength(1);
195+
expect(recommendations[0].region).toBe('region2');
196+
expect(debugMock).toHaveBeenCalledTimes(1);
197+
});
198+
199+
test('should return empty array when every recommendation summary region is denied', async () => {
200+
openopsCommonMock.makeAwsRequest
201+
.mockRejectedValueOnce(new Error('AccessDenied'))
202+
.mockRejectedValueOnce(new Error('UnauthorizedOperation'));
203+
204+
const recommendations = await getRecommendationSummaries(CREDENTIALS, [
205+
'region1',
206+
'region2',
207+
]);
208+
209+
expect(recommendations).toEqual([]);
210+
expect(debugMock).toHaveBeenCalledTimes(2);
211+
});
163212
});
164213

165214
function createRecommendationsSummaryResponse(

packages/blocks/aws-compute-optimizer/test/compute-optimizer-ebs-client.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
const debugMock = jest.fn();
2+
3+
jest.mock('@openops/server-shared', () => {
4+
const actual = jest.requireActual('@openops/server-shared');
5+
6+
return {
7+
...actual,
8+
logger: {
9+
...actual.logger,
10+
debug: debugMock,
11+
},
12+
};
13+
});
14+
115
const CREDENTIALS = {
216
accessKeyId: 'some accessKeyId',
317
secretAccessKey: 'some secretAccessKey',
@@ -156,6 +170,41 @@ describe('Get ebs volumes recommendations', () => {
156170
expect(recommendations.length).toBe(0);
157171
});
158172

173+
test('should skip permission denied regions for EBS recommendations', async () => {
174+
const recommendationsInRegion = createRecommendationsResponse([
175+
createRecommendations('arn:aws:ec2:us-east-1:123456789123:volume/vol-1'),
176+
]);
177+
178+
sendMock
179+
.mockRejectedValueOnce(new Error('AccessDenied'))
180+
.mockResolvedValueOnce([recommendationsInRegion]);
181+
182+
const recommendations = await getEbsRecommendationsForRegions(
183+
CREDENTIALS,
184+
EBSFinding.OPTIMIZED,
185+
['eu-west-1', 'us-east-1'],
186+
);
187+
188+
expect(recommendations.map((result) => result.arn)).toEqual([
189+
'arn:aws:ec2:us-east-1:123456789123:volume/vol-1',
190+
]);
191+
});
192+
193+
test('should return empty array when all EBS recommendation regions are denied', async () => {
194+
sendMock
195+
.mockRejectedValueOnce(new Error('AccessDenied'))
196+
.mockRejectedValueOnce(new Error('UnauthorizedOperation'));
197+
198+
const recommendations = await getEbsRecommendationsForRegions(
199+
CREDENTIALS,
200+
EBSFinding.NOT_OPTIMIZED,
201+
['eu-west-1', 'us-east-1'],
202+
);
203+
204+
expect(recommendations).toEqual([]);
205+
expect(debugMock).toHaveBeenCalledTimes(2);
206+
});
207+
159208
test('should return all the Ebs Volumes Recommendations for the provided volumes', async () => {
160209
const recommendationsInRegion1 = createRecommendationsResponse([
161210
createRecommendations('arn:aws:ec2:us-east-1:123456789123:volume/vol-1'),
@@ -222,6 +271,31 @@ describe('Get ebs volumes recommendations', () => {
222271
expect(recommendations.length).toBe(0);
223272
});
224273

274+
test('should skip permission denied arn regions for EBS recommendations', async () => {
275+
sendMock
276+
.mockResolvedValueOnce([
277+
createRecommendationsResponse([
278+
createRecommendations(
279+
'arn:aws:ec2:us-east-1:123456789123:volume/vol-1',
280+
),
281+
]),
282+
])
283+
.mockRejectedValueOnce(new Error('AccessDenied'));
284+
285+
const recommendations = await getEbsRecommendationsForARNs(
286+
CREDENTIALS,
287+
EBSFinding.OPTIMIZED,
288+
[
289+
'arn:aws:ec2:us-east-1:123456789123:volume/vol-1',
290+
'arn:aws:ec2:us-east-2:123456789123:volume/vol-2',
291+
],
292+
);
293+
294+
expect(recommendations.map((result) => result.arn)).toEqual([
295+
'arn:aws:ec2:us-east-1:123456789123:volume/vol-1',
296+
]);
297+
});
298+
225299
test('should return an empty array when the given volumes have 0 savings recommendations', async () => {
226300
const findingType = EBSFinding.OPTIMIZED;
227301
const recommendationZeroSaving = createRecommendations(

0 commit comments

Comments
 (0)