Skip to content

Commit bd3c9c4

Browse files
committed
Merge branch 'nested-objects-fix' of https://github.com/aws-cloudformation/cloudformation-languageserver into nested-objects-fix
2 parents e6a399e + c2091e1 commit bd3c9c4

File tree

13 files changed

+488
-19
lines changed

13 files changed

+488
-19
lines changed

src/handlers/StackHandler.ts

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ResponseError, ErrorCodes, RequestHandler } from 'vscode-languageserver';
22
import { TopLevelSection } from '../context/ContextType';
33
import { getEntityMap } from '../context/SectionContextBuilder';
4-
import { Parameter } from '../context/semantic/Entity';
4+
import { Parameter, Resource } from '../context/semantic/Entity';
55
import { parseIdentifiable } from '../protocol/LspParser';
66
import { Identifiable } from '../protocol/LspTypes';
77
import { ServerComponents } from '../server/ServerComponents';
@@ -16,6 +16,7 @@ import {
1616
GetStackActionStatusResult,
1717
DescribeValidationStatusResult,
1818
DescribeDeploymentStatusResult,
19+
GetTemplateResourcesResult,
1920
} from '../stacks/actions/StackActionRequestType';
2021
import { ListStacksParams, ListStacksResult } from '../stacks/StackRequestType';
2122
import { LoggerFactory } from '../telemetry/LoggerFactory';
@@ -164,6 +165,72 @@ export function getCapabilitiesHandler(
164165
};
165166
}
166167

168+
export function getTemplateResourcesHandler(
169+
components: ServerComponents,
170+
): RequestHandler<TemplateUri, GetTemplateResourcesResult, void> {
171+
return (rawParams) => {
172+
log.debug({ Handler: 'getTemplateResourcesHandler', rawParams });
173+
174+
try {
175+
const params = parseWithPrettyError(parseTemplateUriParams, rawParams);
176+
const syntaxTree = components.syntaxTreeManager.getSyntaxTree(params);
177+
if (!syntaxTree) return { resources: [] };
178+
179+
const resourcesMap = getEntityMap(syntaxTree, TopLevelSection.Resources);
180+
if (!resourcesMap) return { resources: [] };
181+
182+
const schemas = components.schemaRetriever.getDefault();
183+
const resources = [...resourcesMap.values()].flatMap((context) => {
184+
const resource = context.entity as Resource;
185+
const resourceType = resource.Type ?? '';
186+
if (!resourceType) return [];
187+
188+
const schema = schemas.schemas.get(resourceType);
189+
const primaryIdentifierKeys = extractPrimaryIdentifierKeys(schema?.primaryIdentifier);
190+
const primaryIdentifier = primaryIdentifierKeys
191+
? buildPrimaryIdentifierFromMetadata(resource.Metadata?.PrimaryIdentifier, primaryIdentifierKeys)
192+
: undefined;
193+
194+
return [
195+
{
196+
logicalId: resource.name,
197+
type: resourceType,
198+
primaryIdentifierKeys,
199+
primaryIdentifier,
200+
},
201+
];
202+
});
203+
204+
return { resources };
205+
} catch (error) {
206+
handleStackActionError(error, 'Failed to get template resources');
207+
}
208+
};
209+
}
210+
211+
function extractPrimaryIdentifierKeys(primaryIdentifierPaths?: string[]): string[] | undefined {
212+
return primaryIdentifierPaths
213+
?.map((path) => {
214+
const match = path.match(/\/properties\/(.+)/);
215+
return match?.[1];
216+
})
217+
.filter((key): key is string => key !== undefined);
218+
}
219+
220+
function buildPrimaryIdentifierFromMetadata(
221+
metadataValue: unknown,
222+
keys: string[],
223+
): Record<string, string> | undefined {
224+
if (!metadataValue || keys.length === 0 || typeof metadataValue !== 'string') return undefined;
225+
226+
const values = metadataValue.split('|').map((v) => v.trim());
227+
const identifier: Record<string, string> = {};
228+
for (const [index, key] of keys.entries()) {
229+
identifier[key] = values[index] || values[0];
230+
}
231+
return identifier;
232+
}
233+
167234
export function listStacksHandler(
168235
components: ServerComponents,
169236
): RequestHandler<ListStacksParams, ListStacksResult, void> {

src/protocol/LspStackHandlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
GetParametersRequest,
99
DescribeValidationStatusRequest,
1010
DescribeDeploymentStatusRequest,
11+
GetTemplateResourcesRequest,
1112
} from '../stacks/actions/StackActionProtocol';
1213
import {
1314
TemplateUri,
@@ -18,6 +19,7 @@ import {
1819
GetCapabilitiesResult,
1920
DescribeValidationStatusResult,
2021
DescribeDeploymentStatusResult,
22+
GetTemplateResourcesResult,
2123
} from '../stacks/actions/StackActionRequestType';
2224
import {
2325
ListStacksParams,
@@ -64,6 +66,10 @@ export class LspStackHandlers {
6466
this.connection.onRequest(GetCapabilitiesRequest.method, handler);
6567
}
6668

69+
onGetTemplateResources(handler: RequestHandler<TemplateUri, GetTemplateResourcesResult, void>) {
70+
this.connection.onRequest(GetTemplateResourcesRequest.method, handler);
71+
}
72+
6773
onListStacks(handler: RequestHandler<ListStacksParams, ListStacksResult, void>) {
6874
this.connection.onRequest(ListStacksRequest.method, handler);
6975
}

src/server/CfnServer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
getCapabilitiesHandler,
3636
describeValidationStatusHandler,
3737
describeDeploymentStatusHandler,
38+
getTemplateResourcesHandler,
3839
} from '../handlers/StackHandler';
3940
import { LspComponents } from '../protocol/LspComponents';
4041
import { closeSafely } from '../utils/Closeable';
@@ -100,6 +101,7 @@ export class CfnServer {
100101
this.lsp.stackHandlers.onGetParameters(getParametersHandler(this.components));
101102
this.lsp.stackHandlers.onCreateValidation(createValidationHandler(this.components));
102103
this.lsp.stackHandlers.onGetCapabilities(getCapabilitiesHandler(this.components));
104+
this.lsp.stackHandlers.onGetTemplateResources(getTemplateResourcesHandler(this.components));
103105
this.lsp.stackHandlers.onCreateDeployment(createDeploymentHandler(this.components));
104106
this.lsp.stackHandlers.onGetValidationStatus(getValidationStatusHandler(this.components));
105107
this.lsp.stackHandlers.onGetDeploymentStatus(getDeploymentStatusHandler(this.components));

src/services/CfnService.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
Capability,
3434
StackResourceDriftStatus,
3535
Parameter,
36+
ResourceToImport,
3637
RegistryType,
3738
ValidateTemplateCommand,
3839
ValidateTemplateInput,
@@ -45,6 +46,7 @@ import {
4546
waitUntilChangeSetCreateComplete,
4647
waitUntilStackUpdateComplete,
4748
waitUntilStackCreateComplete,
49+
waitUntilStackImportComplete,
4850
DescribeChangeSetCommandOutput,
4951
Change,
5052
GetTemplateCommand,
@@ -122,6 +124,7 @@ export class CfnService {
122124
Parameters?: Parameter[];
123125
Capabilities?: Capability[];
124126
ChangeSetType?: 'CREATE' | 'UPDATE' | 'IMPORT';
127+
ResourcesToImport?: ResourceToImport[];
125128
}): Promise<CreateChangeSetCommandOutput> {
126129
return await this.withClient((client) => client.send(new CreateChangeSetCommand(params)));
127130
}
@@ -344,6 +347,19 @@ export class CfnService {
344347
});
345348
}
346349

350+
public async waitUntilStackImportComplete(
351+
params: DescribeStacksCommandInput,
352+
timeoutMinutes: number = 30,
353+
): Promise<WaiterResult> {
354+
return await this.withClient(async (client) => {
355+
const waiterConfig: WaiterConfiguration<CloudFormationClient> = {
356+
client,
357+
maxWaitTime: timeoutMinutes * 60,
358+
};
359+
return await waitUntilStackImportComplete(waiterConfig, params);
360+
});
361+
}
362+
347363
public async validateTemplate(params: ValidateTemplateInput): Promise<ValidateTemplateOutput> {
348364
return await this.withClient((client) => client.send(new ValidateTemplateCommand(params)));
349365
}

src/stacks/actions/DeploymentWorkflow.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@ export class DeploymentWorkflow implements StackActionWorkflow<DescribeDeploymen
3535
) {}
3636

3737
async start(params: CreateStackActionParams): Promise<CreateStackActionResult> {
38-
// Check if stack exists to determine CREATE vs UPDATE
39-
let changeSetType: ChangeSetType = ChangeSetType.CREATE;
40-
try {
41-
await this.cfnService.describeStacks({ StackName: params.stackName });
42-
changeSetType = ChangeSetType.UPDATE;
43-
} catch {
44-
changeSetType = ChangeSetType.CREATE;
38+
// Determine ChangeSet type based on resourcesToImport and stack existence
39+
let changeSetType: ChangeSetType;
40+
41+
if (params.resourcesToImport && params.resourcesToImport.length > 0) {
42+
changeSetType = ChangeSetType.IMPORT;
43+
} else {
44+
try {
45+
await this.cfnService.describeStacks({ StackName: params.stackName });
46+
changeSetType = ChangeSetType.UPDATE;
47+
} catch {
48+
changeSetType = ChangeSetType.CREATE;
49+
}
4550
}
4651

4752
const changeSetName = await processChangeSet(this.cfnService, this.documentManager, params, changeSetType);

src/stacks/actions/StackActionOperations.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export async function processChangeSet(
3737
Parameters: params.parameters,
3838
Capabilities: params.capabilities,
3939
ChangeSetType: changeSetType,
40+
ResourcesToImport: params.resourcesToImport,
4041
});
4142

4243
return changeSetName;
@@ -98,9 +99,13 @@ export async function waitForDeployment(
9899
? await cfnService.waitUntilStackCreateComplete({
99100
StackName: stackName,
100101
})
101-
: await cfnService.waitUntilStackUpdateComplete({
102-
StackName: stackName,
103-
});
102+
: changeSetType === ChangeSetType.IMPORT
103+
? await cfnService.waitUntilStackImportComplete({
104+
StackName: stackName,
105+
})
106+
: await cfnService.waitUntilStackUpdateComplete({
107+
StackName: stackName,
108+
});
104109

105110
if (result.state === WaiterState.SUCCESS) {
106111
return {

src/stacks/actions/StackActionParser.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,19 @@ const ParameterSchema = z.object({
1515
ResolvedValue: z.string().optional(),
1616
});
1717

18+
const ResourceToImportSchema = z.object({
19+
ResourceType: z.string(),
20+
LogicalResourceId: z.string(),
21+
ResourceIdentifier: z.record(z.string(), z.string()),
22+
});
23+
1824
const StackActionParamsSchema = z.object({
1925
id: z.string().min(1),
2026
uri: z.string().min(1),
2127
stackName: z.string().min(1).max(128),
2228
parameters: z.array(ParameterSchema).optional(),
2329
capabilities: z.array(CapabilitySchema).optional(),
30+
resourcesToImport: z.array(ResourceToImportSchema).optional(),
2431
});
2532

2633
const TemplateUriSchema = z.string().min(1);

src/stacks/actions/StackActionProtocol.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
GetCapabilitiesResult,
1010
DescribeValidationStatusResult,
1111
DescribeDeploymentStatusResult,
12+
GetTemplateResourcesResult,
1213
} from './StackActionRequestType';
1314

1415
export const CreateValidationRequest = new RequestType<CreateStackActionParams, CreateStackActionResult, void>(
@@ -40,3 +41,7 @@ export const GetParametersRequest = new RequestType<TemplateUri, GetParametersRe
4041
export const GetCapabilitiesRequest = new RequestType<TemplateUri, GetCapabilitiesResult, void>(
4142
'aws/cfn/stack/capabilities',
4243
);
44+
45+
export const GetTemplateResourcesRequest = new RequestType<TemplateUri, GetTemplateResourcesResult, void>(
46+
'aws/cfn/stack/import/resources',
47+
);

src/stacks/actions/StackActionRequestType.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,18 @@ import { DateTime } from 'luxon';
99
import { Parameter as EntityParameter } from '../../context/semantic/Entity';
1010
import { Identifiable } from '../../protocol/LspTypes';
1111

12+
export type ResourceToImport = {
13+
ResourceType: string;
14+
LogicalResourceId: string;
15+
ResourceIdentifier: Record<string, string>;
16+
};
17+
1218
export type CreateStackActionParams = Identifiable & {
1319
uri: string;
1420
stackName: string;
1521
parameters?: Parameter[];
1622
capabilities?: Capability[];
23+
resourcesToImport?: ResourceToImport[];
1724
};
1825

1926
export type CreateStackActionResult = Identifiable & {
@@ -31,6 +38,17 @@ export type GetCapabilitiesResult = {
3138
capabilities: Capability[];
3239
};
3340

41+
export type TemplateResource = {
42+
logicalId: string;
43+
type: string;
44+
primaryIdentifierKeys?: string[];
45+
primaryIdentifier?: Record<string, string>;
46+
};
47+
48+
export type GetTemplateResourcesResult = {
49+
resources: TemplateResource[];
50+
};
51+
3452
export type StackChange = {
3553
type?: string;
3654
resourceChange?: {

src/stacks/actions/ValidationWorkflow.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,18 @@ export class ValidationWorkflow implements StackActionWorkflow<DescribeValidatio
4444
) {}
4545

4646
async start(params: CreateStackActionParams): Promise<CreateStackActionResult> {
47-
// Check if stack exists to determine CREATE vs UPDATE
48-
let changeSetType: ChangeSetType = ChangeSetType.CREATE;
49-
try {
50-
await this.cfnService.describeStacks({ StackName: params.stackName });
51-
changeSetType = ChangeSetType.UPDATE;
52-
} catch {
53-
changeSetType = ChangeSetType.CREATE;
47+
// Determine ChangeSet type based on resourcesToImport and stack existence
48+
let changeSetType: ChangeSetType;
49+
50+
if (params.resourcesToImport && params.resourcesToImport.length > 0) {
51+
changeSetType = ChangeSetType.IMPORT;
52+
} else {
53+
try {
54+
await this.cfnService.describeStacks({ StackName: params.stackName });
55+
changeSetType = ChangeSetType.UPDATE;
56+
} catch {
57+
changeSetType = ChangeSetType.CREATE;
58+
}
5459
}
5560

5661
const changeSetName = await processChangeSet(this.cfnService, this.documentManager, params, changeSetType);

0 commit comments

Comments
 (0)