Skip to content

Commit a69c6b2

Browse files
authored
Handle private resources lacking list handler and PrivateTypeException (#241)
1 parent 565bf7c commit a69c6b2

File tree

2 files changed

+84
-13
lines changed

2 files changed

+84
-13
lines changed

src/resourceState/ResourceStateManager.ts

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { GetResourceCommandOutput, ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';
1+
import {
2+
GetResourceCommandOutput,
3+
PrivateTypeException,
4+
ResourceNotFoundException,
5+
} from '@aws-sdk/client-cloudcontrol';
26
import { DateTime } from 'luxon';
37
import { SchemaRetriever } from '../schema/SchemaRetriever';
48
import { CfnExternal } from '../server/CfnExternal';
@@ -10,6 +14,7 @@ import { LoggerFactory } from '../telemetry/LoggerFactory';
1014
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
1115
import { Telemetry, Measure } from '../telemetry/TelemetryDecorator';
1216
import { Closeable } from '../utils/Closeable';
17+
import { handleLspError } from '../utils/Errors';
1318
import { NO_LIST_SUPPORT, REQUIRES_RESOURCE_MODEL } from './ListResourcesExclusionTypes';
1419
import { ListResourcesResult, RefreshResourcesResult } from './ResourceStateTypes';
1520

@@ -141,7 +146,7 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
141146
return { found: true, resourceList: cached };
142147
}
143148

144-
// Create new cache entry if doesn't exist
149+
// Create new cache entry if it doesn't exist
145150
if (!cached) {
146151
const newList: ResourceList = {
147152
typeName,
@@ -158,9 +163,11 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
158163
}
159164

160165
public getResourceTypes(): string[] {
161-
const schemas = this.schemaRetriever.getDefault().schemas;
162-
const allTypes = new Set(schemas.keys());
163-
return [...allTypes].filter((type) => !NO_LIST_SUPPORT.has(type) && !REQUIRES_RESOURCE_MODEL.has(type));
166+
const schemas = [...this.schemaRetriever.getDefault().schemas.values()];
167+
const listableTypes = schemas
168+
.filter((schema) => schema.handlers?.list !== undefined)
169+
.map((schema) => schema.typeName);
170+
return [...listableTypes].filter((type) => !NO_LIST_SUPPORT.has(type) && !REQUIRES_RESOURCE_MODEL.has(type));
164171
}
165172

166173
private storeResourceState(typeName: ResourceType, id: ResourceId, state: ResourceState) {
@@ -214,7 +221,7 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
214221
};
215222
} catch (error) {
216223
log.error(error, `CCAPI ListResource failed for type ${typeName}`);
217-
return;
224+
this.handleListExceptions(error, typeName);
218225
}
219226
}
220227

@@ -277,6 +284,16 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
277284
}
278285
}
279286

287+
private handleListExceptions(error: unknown, typeName: string) {
288+
if (error instanceof PrivateTypeException) {
289+
log.error(error, `Failed to list private resource`);
290+
handleLspError(
291+
error,
292+
`Failed to list identifiers for ${typeName}. Cloud Control API hasn't received a valid response from the resource handler, due to a configuration error. This includes issues such as the resource handler returning an invalid response, or timing out.`,
293+
);
294+
}
295+
}
296+
280297
configure(settingsManager: ISettingsSubscriber) {
281298
if (this.settingsSubscription) {
282299
this.settingsSubscription.unsubscribe();

tst/unit/resourceState/ResourceStateManager.test.ts

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1-
import { GetResourceCommandOutput, ResourceNotFoundException } from '@aws-sdk/client-cloudcontrol';
1+
import {
2+
GetResourceCommandOutput,
3+
PrivateTypeException,
4+
ResourceNotFoundException,
5+
} from '@aws-sdk/client-cloudcontrol';
26
import { DateTime } from 'luxon';
37
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
8+
import { ResponseError } from 'vscode-languageserver';
49
import { ResourceStateManager } from '../../../src/resourceState/ResourceStateManager';
510
import { CombinedSchemas } from '../../../src/schema/CombinedSchemas';
611
import { CcapiService } from '../../../src/services/CcapiService';
712
import { S3Service } from '../../../src/services/S3Service';
813
import { createMockSchemaRetriever } from '../../utils/MockServerComponents';
14+
import { combinedSchemas } from '../../utils/SchemaUtils';
915

1016
describe('ResourceStateManager', () => {
1117
const mockCcapiService = {
@@ -175,6 +181,19 @@ describe('ResourceStateManager', () => {
175181

176182
expect(result).toBeUndefined();
177183
});
184+
185+
it('should handle private resource exceptions', async () => {
186+
const error = new PrivateTypeException({
187+
message: 'Private type error',
188+
$metadata: {},
189+
});
190+
vi.mocked(mockCcapiService.listResources).mockRejectedValue(error);
191+
192+
await expect(manager.listResources('MyOrg::Custom::Resource')).rejects.toThrow(ResponseError);
193+
await expect(manager.listResources('MyOrg::Custom::Resource')).rejects.toThrow(
194+
"Failed to list identifiers for MyOrg::Custom::Resource. Cloud Control API hasn't received a valid response from the resource handler, due to a configuration error. This includes issues such as the resource handler returning an invalid response, or timing out.",
195+
);
196+
});
178197
});
179198

180199
describe('searchResourceByIdentifier()', () => {
@@ -427,9 +446,9 @@ describe('ResourceStateManager', () => {
427446
it('should filter out resource types without list support', () => {
428447
const mockSchemas: CombinedSchemas = {
429448
schemas: new Map([
430-
['AWS::S3::Bucket', {}],
431-
['AWS::IAM::Role', {}],
432-
['AWS::IAM::RolePolicy', {}],
449+
['AWS::S3::Bucket', { typeName: 'AWS::S3::Bucket', handlers: { list: {} } }],
450+
['AWS::IAM::Role', { typeName: 'AWS::IAM::Role', handlers: { list: {} } }],
451+
['AWS::IAM::RolePolicy', { typeName: 'AWS::IAM::RolePolicy', handlers: { list: {} } }],
433452
]),
434453
} as CombinedSchemas;
435454
const managerWithSchemas = new ResourceStateManager(
@@ -448,9 +467,9 @@ describe('ResourceStateManager', () => {
448467
it('should filter out resource types requiring resource model properties', () => {
449468
const mockSchemas: CombinedSchemas = {
450469
schemas: new Map([
451-
['AWS::S3::Bucket', {}],
452-
['AWS::EKS::Cluster', {}],
453-
['AWS::EKS::AddOn', {}],
470+
['AWS::S3::Bucket', { typeName: 'AWS::S3::Bucket', handlers: { list: {} } }],
471+
['AWS::EKS::Cluster', { typeName: 'AWS::EKS::Cluster', handlers: { list: {} } }],
472+
['AWS::EKS::AddOn', { typeName: 'AWS::EKS::AddOn', handlers: { list: {} } }],
454473
]),
455474
} as CombinedSchemas;
456475
const managerWithSchemas = new ResourceStateManager(
@@ -465,5 +484,40 @@ describe('ResourceStateManager', () => {
465484
expect(result).toContain('AWS::EKS::Cluster');
466485
expect(result).not.toContain('AWS::EKS::AddOn');
467486
});
487+
488+
it('should return all supported public types', () => {
489+
const testSchemas = combinedSchemas();
490+
const resourceManagerWithRealSchemas = new ResourceStateManager(
491+
mockCcapiService,
492+
createMockSchemaRetriever(testSchemas),
493+
mockS3Service,
494+
);
495+
496+
const result = resourceManagerWithRealSchemas.getResourceTypes();
497+
498+
expect(result).toContain('AWS::S3::Bucket');
499+
expect(result).toContain('AWS::IAM::Role');
500+
expect(result).toContain('AWS::Lambda::Function');
501+
expect(result.every((type) => type.startsWith('AWS::'))).toBe(true);
502+
});
503+
504+
it('should not return private resource types with no list handler permissions', () => {
505+
const mockSchemas: CombinedSchemas = {
506+
schemas: new Map([
507+
['AWS::S3::Bucket', { typeName: 'AWS::S3::Bucket', handlers: { list: {} } }],
508+
['MyOrg::Custom::Resource', { typeName: 'MyOrg::Custom::Resource' }],
509+
]),
510+
} as CombinedSchemas;
511+
const managerWithSchemas = new ResourceStateManager(
512+
mockCcapiService,
513+
createMockSchemaRetriever(mockSchemas),
514+
mockS3Service,
515+
);
516+
517+
const result = managerWithSchemas.getResourceTypes();
518+
519+
expect(result).toContain('AWS::S3::Bucket');
520+
expect(result).not.toContain('MyOrg::Custom::Resource');
521+
});
468522
});
469523
});

0 commit comments

Comments
 (0)