Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/autocomplete/ResourceStateCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,18 @@ export class ResourceStateCompletionProvider implements CompletionProvider {
}

log.info(`Retrieving resource details from AWS account with id: ${identifier} and type: ${resource.Type}`);
const resourceState = await this.resourceStateManager.getResource(resource.Type, identifier);
const properties = resourceState?.properties;
if (!properties) {
let properties: string;
try {
const result = await this.resourceStateManager.getResource(resource.Type, identifier);
if (!result.resource) {
return [];
}
properties = result.resource.properties;
} catch {
log.info(`No resource found for id: ${identifier} and type: ${resource.Type}`);
return [];
}

const propertiesObj = JSON.parse(properties) as Record<string, string>;
this.applyTransformers(propertiesObj, schema);
this.removeExistingProperties(propertiesObj, resource);
Expand Down
1 change: 1 addition & 0 deletions src/handlers/ResourceHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export function searchResourceHandler(
nextToken: result.resourceList.nextToken,
}
: undefined,
error: result.error,
};
};
}
Expand Down
64 changes: 38 additions & 26 deletions src/resourceState/ResourceStateImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { CfnLspProviders } from '../server/CfnLspProviders';
import { LoggerFactory } from '../telemetry/LoggerFactory';
import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
import { Telemetry, Measure } from '../telemetry/TelemetryDecorator';
import { extractErrorMessage } from '../utils/Errors';
import { getIndentationString } from '../utils/IndentationUtils';
import { ResourceStateManager } from './ResourceStateManager';
import {
Expand Down Expand Up @@ -156,6 +157,7 @@ export class ResourceStateImporter {
parentResourceType?: string,
): Promise<{ fetchedResourceStates: ResourceTemplateFormat[]; importResult: ResourceStateResult }> {
const fetchedResourceStates: ResourceTemplateFormat[] = [];
const failureReasons: Record<string, Record<string, string>> = {};
const importResult: ResourceStateResult = {
completionItem: undefined,
failedImports: {},
Expand All @@ -175,40 +177,50 @@ export class ResourceStateImporter {
}
for (const resourceIdentifier of resourceSelection.resourceIdentifiers) {
try {
const resourceState = await this.resourceStateManager.getResource(resourceType, resourceIdentifier);
if (resourceState) {
this.getOrCreate(importResult.successfulImports, resourceType, []).push(resourceIdentifier);
const logicalId = this.generateUniqueLogicalId(
resourceType,
resourceIdentifier,
syntaxTree,
generatedLogicalIds,
parentResourceType,
);
generatedLogicalIds.add(logicalId);
fetchedResourceStates.push({
[logicalId]: {
Type: resourceType,
DeletionPolicy:
purpose === ResourceStatePurpose.IMPORT ? DeletionPolicyOnImport : undefined,
Properties: this.applyTransformations(
resourceState.properties,
schema,
purpose,
logicalId,
),
Metadata: await this.createMetadata(resourceIdentifier, purpose),
},
});
} else {
const result = await this.resourceStateManager.getResource(resourceType, resourceIdentifier);
if (!result.resource) {
this.getOrCreate(importResult.failedImports, resourceType, []).push(resourceIdentifier);
if (result.error) {
failureReasons[resourceType] ??= {};
failureReasons[resourceType][resourceIdentifier] = result.error;
}
continue;
}
this.getOrCreate(importResult.successfulImports, resourceType, []).push(resourceIdentifier);
const logicalId = this.generateUniqueLogicalId(
resourceType,
resourceIdentifier,
syntaxTree,
generatedLogicalIds,
parentResourceType,
);
generatedLogicalIds.add(logicalId);
fetchedResourceStates.push({
[logicalId]: {
Type: resourceType,
DeletionPolicy:
purpose === ResourceStatePurpose.IMPORT ? DeletionPolicyOnImport : undefined,
Properties: this.applyTransformations(
result.resource.properties,
schema,
purpose,
logicalId,
),
Metadata: await this.createMetadata(resourceIdentifier, purpose),
},
});
} catch (error) {
log.error(error, `Error importing resource state for ${resourceType} id: ${resourceIdentifier}`);
this.getOrCreate(importResult.failedImports, resourceType, []).push(resourceIdentifier);
const errorMessage = extractErrorMessage(error);
failureReasons[resourceType] ??= {};
failureReasons[resourceType][resourceIdentifier] = errorMessage;
}
}
}
if (Object.keys(failureReasons).length > 0) {
importResult.failureReasons = failureReasons;
}
return { fetchedResourceStates, importResult };
}

Expand Down
32 changes: 21 additions & 11 deletions src/resourceState/ResourceStateManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ScopedTelemetry } from '../telemetry/ScopedTelemetry';
import { Telemetry, Measure, Count } from '../telemetry/TelemetryDecorator';
import { isClientError } from '../utils/AwsErrorMapper';
import { Closeable } from '../utils/Closeable';
import { extractErrorMessage } from '../utils/Errors';
import { NO_LIST_SUPPORT, REQUIRES_RESOURCE_MODEL } from './ListResourcesExclusionTypes';
import { ListResourcesResult, RefreshResourcesResult } from './ResourceStateTypes';

Expand All @@ -27,6 +28,8 @@ export type ResourceState = {
createdTimestamp: DateTime;
};

export type GetResourceResult = { resource?: ResourceState; error?: string };

type ResourceList = {
typeName: string;
resourceIdentifiers: string[];
Expand Down Expand Up @@ -60,12 +63,13 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
}

@Measure({ name: 'getResource', captureErrorType: true })
public async getResource(typeName: ResourceType, identifier: ResourceId): Promise<ResourceState | undefined> {
public async getResource(typeName: ResourceType, identifier: ResourceId): Promise<GetResourceResult> {
Comment thread
gemammercado marked this conversation as resolved.
const cachedResources = this.getResourceState(typeName, identifier);
if (cachedResources) {
this.telemetry.count('state.hit', 1);
return cachedResources;
return { resource: cachedResources };
}

this.telemetry.count('state.miss', 1);

let output: GetResourceCommandOutput | undefined = undefined;
Expand All @@ -75,20 +79,20 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
} catch (error) {
if (error instanceof ResourceNotFoundException) {
log.info(`No resource found for type ${typeName} and identifier "${identifier}"`);
Comment thread
gemammercado marked this conversation as resolved.
return;
}
if (isClientError(error)) {
return { error: extractErrorMessage(error) };
} else if (isClientError(error)) {
log.info(`Client error for type ${typeName} and identifier "${identifier}"`);
return;
return { error: extractErrorMessage(error) };
}
log.error(error, `CCAPI GetResource failed for type ${typeName} and identifier "${identifier}"`);
throw error;
}

if (!output?.TypeName || !output?.ResourceDescription?.Identifier || !output?.ResourceDescription?.Properties) {
log.error(
`GetResource output is missing required fields for type ${typeName} with identifier "${identifier}"`,
);
return;
return { error: 'GetResource output is missing required fields' };
}

const value: ResourceState = {
Expand All @@ -99,7 +103,7 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
};

this.storeResourceState(typeName, identifier, value);
return value;
return { resource: value };
}

@Measure({ name: 'listResources' })
Expand Down Expand Up @@ -135,12 +139,18 @@ export class ResourceStateManager implements SettingsConfigurable, Closeable {
public async searchResourceByIdentifier(
typeName: string,
identifier: string,
): Promise<{ found: boolean; resourceList?: ResourceList }> {
const resource = await this.getResource(typeName, identifier);
if (!resource) {
): Promise<{ found: boolean; resourceList?: ResourceList; error?: string }> {
let result: GetResourceResult;
try {
result = await this.getResource(typeName, identifier);
} catch {
return { found: false };
}

if (!result.resource) {
return { found: false, error: result.error };
}

// Add to cache
const cached = this.resourceListMap.get(typeName);
if (cached && !cached.resourceIdentifiers.includes(identifier)) {
Expand Down
2 changes: 2 additions & 0 deletions src/resourceState/ResourceStateTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface ResourceStateResult {
completionItem?: CompletionItem;
successfulImports: Record<ResourceType, ResourceIdentifier[]>;
failedImports: Record<ResourceType, ResourceIdentifier[]>;
failureReasons?: Record<ResourceType, Record<ResourceIdentifier, string>>;
warning?: string;
}

Expand Down Expand Up @@ -88,6 +89,7 @@ export type SearchResourceParams = {
export type SearchResourceResult = {
found: boolean;
resource?: ResourceSummary;
error?: string;
};

export const SearchResourceRequest = new RequestType<SearchResourceParams, SearchResourceResult, void>(
Expand Down
2 changes: 1 addition & 1 deletion src/schema/ResourceSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ export class ResourceSchema {
}

const parts = path.split('/');
let current: any = this as unknown as any; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion
let current: any = this as any;

for (const part of parts) {
if (!current || typeof current !== 'object') {
Expand Down
95 changes: 51 additions & 44 deletions tst/unit/autocomplete/ResourceStateCompletionProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('ResourceStateCompletionProvider', () => {
});

mockComponents.schemaRetriever.getDefault.returns(s3Schemas);
mockComponents.resourceStateManager.getResource.resolves(undefined);
mockComponents.resourceStateManager.getResource.resolves({ error: 'Resource not found' });

const result = await provider.getCompletions(context, mockYamlParams);

Expand All @@ -132,12 +132,7 @@ describe('ResourceStateCompletionProvider', () => {
});

mockComponents.schemaRetriever.getDefault.returns(s3Schemas);
mockComponents.resourceStateManager.getResource.resolves({
typeName: 'AWS::S3::Bucket',
identifier: 'test',
properties: '',
createdTimestamp: new Date() as any,
});
mockComponents.resourceStateManager.getResource.rejects(new Error('Invalid resource state'));

const result = await provider.getCompletions(context, mockYamlParams);

Expand All @@ -157,14 +152,16 @@ describe('ResourceStateCompletionProvider', () => {
emptySchemas.schemas.set('Custom::Type', customTypeSchema);
mockComponents.schemaRetriever.getDefault.returns(emptySchemas);
mockComponents.resourceStateManager.getResource.resolves({
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
ExistingProp: 'value',
}),
createdTimestamp: new Date() as any,
resource: {
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
ExistingProp: 'value',
}),
createdTimestamp: new Date() as any,
},
});

const result = await provider.getCompletions(context, mockYamlParams);
Expand Down Expand Up @@ -192,13 +189,15 @@ describe('ResourceStateCompletionProvider', () => {
emptySchemas.schemas.set('Custom::Type', customTypeSchema);
mockComponents.schemaRetriever.getDefault.returns(emptySchemas);
mockComponents.resourceStateManager.getResource.resolves({
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
}),
createdTimestamp: new Date() as any,
resource: {
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
}),
createdTimestamp: new Date() as any,
},
});

const result = await provider.getCompletions(context, mockYamlParams);
Expand Down Expand Up @@ -273,10 +272,12 @@ describe('ResourceStateCompletionProvider', () => {
});

mockComponents.resourceStateManager.getResource.resolves({
typeName: 'AWS::IAM::Role',
identifier: 'Admin',
properties: `{"Path":"/","ManagedPolicyArns":["arn:aws:iam::aws:policy/AdministratorAccess"],"MaxSessionDuration":43200,"RoleName":"Admin","AssumeRolePolicyDocument":{"Version":"2012-10-17","Statement":[{"Condition":{"StringEquals":{"sts:ExternalId":"IsengardExternalIdAKj8duTfSqL6"}},"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::727820809195:root"},"Sid":""}]},"Arn":"arn:aws:iam::783764615233:role/Admin","RoleId":"AROA3M7AC6BAWIZG2LLQY"}`,
createdTimestamp: DateTime.now(),
resource: {
typeName: 'AWS::IAM::Role',
identifier: 'Admin',
properties: `{"Path":"/","ManagedPolicyArns":["arn:aws:iam::aws:policy/AdministratorAccess"],"MaxSessionDuration":43200,"RoleName":"Admin","AssumeRolePolicyDocument":{"Version":"2012-10-17","Statement":[{"Condition":{"StringEquals":{"sts:ExternalId":"IsengardExternalIdAKj8duTfSqL6"}},"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::727820809195:root"},"Sid":""}]},"Arn":"arn:aws:iam::783764615233:role/Admin","RoleId":"AROA3M7AC6BAWIZG2LLQY"}`,
createdTimestamp: DateTime.now(),
},
});

const result = await provider.getCompletions(context, mockJsonParams);
Expand Down Expand Up @@ -312,10 +313,12 @@ describe('ResourceStateCompletionProvider', () => {
});

mockComponents.resourceStateManager.getResource.resolves({
typeName: 'AWS::IAM::Role',
identifier: 'Admin',
properties: `{"Path":"/","ManagedPolicyArns":["arn:aws:iam::aws:policy/AdministratorAccess"],"MaxSessionDuration":43200,"RoleName":"Admin","AssumeRolePolicyDocument":{"Version":"2012-10-17","Statement":[{"Condition":{"StringEquals":{"sts:ExternalId":"IsengardExternalIdAKj8duTfSqL6"}},"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::727820809195:root"},"Sid":""}]},"Arn":"arn:aws:iam::783764615233:role/Admin","RoleId":"AROA3M7AC6BAWIZG2LLQY"}`,
createdTimestamp: DateTime.now(),
resource: {
typeName: 'AWS::IAM::Role',
identifier: 'Admin',
properties: `{"Path":"/","ManagedPolicyArns":["arn:aws:iam::aws:policy/AdministratorAccess"],"MaxSessionDuration":43200,"RoleName":"Admin","AssumeRolePolicyDocument":{"Version":"2012-10-17","Statement":[{"Condition":{"StringEquals":{"sts:ExternalId":"IsengardExternalIdAKj8duTfSqL6"}},"Action":"sts:AssumeRole","Effect":"Allow","Principal":{"AWS":"arn:aws:iam::727820809195:root"},"Sid":""}]},"Arn":"arn:aws:iam::783764615233:role/Admin","RoleId":"AROA3M7AC6BAWIZG2LLQY"}`,
createdTimestamp: DateTime.now(),
},
});

const result = await provider.getCompletions(context, mockYamlParams);
Expand Down Expand Up @@ -464,13 +467,15 @@ describe('ResourceStateCompletionProvider', () => {
emptySchemas.schemas.set('Custom::Type', customTypeSchema);
mockComponents.schemaRetriever.getDefault.returns(emptySchemas);
mockComponents.resourceStateManager.getResource.resolves({
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
}),
createdTimestamp: new Date() as any,
resource: {
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
}),
createdTimestamp: new Date() as any,
},
});

const result = await provider.getCompletions(context, mockYamlParams);
Expand All @@ -495,13 +500,15 @@ describe('ResourceStateCompletionProvider', () => {
mockComponents.schemaRetriever.getDefault.returns(emptySchemas);
mockComponents.documentManager.getLine.returns('"",');
mockComponents.resourceStateManager.getResource.resolves({
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
}),
createdTimestamp: new Date() as any,
resource: {
typeName: 'Custom::Type',
identifier: 'test',
properties: JSON.stringify({
BucketName: 'test',
VersioningConfiguration: { Status: 'Enabled' },
}),
createdTimestamp: new Date() as any,
},
});

const result = await provider.getCompletions(context, mockJsonParams);
Expand Down
Loading
Loading