Skip to content

Commit c1155cc

Browse files
committed
code lens for managed resources, dry run and deployment
1 parent 5f6f422 commit c1155cc

File tree

14 files changed

+614
-1
lines changed

14 files changed

+614
-1
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { CodeLens, Position, Range } from 'vscode-languageserver';
2+
import { TextDocument } from 'vscode-languageserver-textdocument';
3+
import { TopLevelSection } from '../context/ContextType';
4+
import { getEntityMap } from '../context/SectionContextBuilder';
5+
import { Resource } from '../context/semantic/Entity';
6+
import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
7+
import { ServerComponents } from '../server/ServerComponents';
8+
9+
const MANAGED_RESOURCE_CONSTANTS = {
10+
COMMAND_TITLE: 'Open Stack Template',
11+
COMMAND_NAME: 'aws.cloudformation.api.openStackTemplate',
12+
STACK_NAME_REGEX: /^\s*"?StackName"?\s*:/,
13+
} as const;
14+
15+
export class ManagedResourceCodeLens {
16+
constructor(private readonly syntaxTreeManager: SyntaxTreeManager) {}
17+
18+
getCodeLenses(uri: string, document: TextDocument): CodeLens[] {
19+
const lenses: CodeLens[] = [];
20+
21+
const syntaxTree = this.syntaxTreeManager.getSyntaxTree(uri);
22+
if (!syntaxTree) {
23+
return lenses;
24+
}
25+
26+
const resourcesMap = getEntityMap(syntaxTree, TopLevelSection.Resources);
27+
if (!resourcesMap) {
28+
return lenses;
29+
}
30+
31+
const text = document.getText();
32+
const lines = text.split('\n');
33+
34+
for (const [, resourceContext] of resourcesMap) {
35+
const resource = resourceContext.entity as Resource;
36+
const metadata = resource.Metadata;
37+
38+
if (
39+
metadata?.ManagedByStack === true &&
40+
typeof metadata.StackName === 'string' &&
41+
typeof metadata.PrimaryIdentifier === 'string'
42+
) {
43+
const stackName = metadata.StackName;
44+
const primaryIdentifier = metadata.PrimaryIdentifier;
45+
46+
const startRow = resourceContext.startPosition.row;
47+
const endRow = resourceContext.endPosition.row;
48+
49+
for (let i = startRow; i <= endRow && i < lines.length; i++) {
50+
if (MANAGED_RESOURCE_CONSTANTS.STACK_NAME_REGEX.test(lines[i])) {
51+
lenses.push({
52+
range: Range.create(Position.create(i, 0), Position.create(i, 0)),
53+
command: {
54+
title: MANAGED_RESOURCE_CONSTANTS.COMMAND_TITLE,
55+
command: MANAGED_RESOURCE_CONSTANTS.COMMAND_NAME,
56+
arguments: [stackName, primaryIdentifier],
57+
},
58+
});
59+
break;
60+
}
61+
}
62+
}
63+
}
64+
65+
return lenses;
66+
}
67+
68+
static create(components: ServerComponents): ManagedResourceCodeLens {
69+
return new ManagedResourceCodeLens(components.syntaxTreeManager);
70+
}
71+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { CodeLens, Position, Range } from 'vscode-languageserver';
2+
3+
const STACK_ACTION_TITLES = {
4+
DRY_RUN: 'Dry Run Deployment',
5+
DEPLOY: 'Deploy',
6+
} as const;
7+
8+
const STACK_ACTION_COMMANDS = {
9+
VALIDATE: 'aws.cloudformation.api.validateTemplate',
10+
DEPLOY: 'aws.cloudformation.api.deployTemplate',
11+
} as const;
12+
13+
export function getStackActionsCodeLenses(uri: string): CodeLens[] {
14+
const range = Range.create(Position.create(0, 0), Position.create(0, 0));
15+
16+
return [
17+
{
18+
range,
19+
command: {
20+
title: STACK_ACTION_TITLES.DRY_RUN,
21+
command: STACK_ACTION_COMMANDS.VALIDATE,
22+
arguments: [uri],
23+
},
24+
},
25+
{
26+
range,
27+
command: {
28+
title: STACK_ACTION_TITLES.DEPLOY,
29+
command: STACK_ACTION_COMMANDS.DEPLOY,
30+
arguments: [uri],
31+
},
32+
},
33+
];
34+
}

src/handlers/CodeLensHandler.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { CodeLens, CodeLensParams } from 'vscode-languageserver';
2+
import { ServerRequestHandler } from 'vscode-languageserver/lib/common/server';
3+
import { getStackActionsCodeLenses } from '../codeLens/StackActionsCodeLens';
4+
import { ServerComponents } from '../server/ServerComponents';
5+
import { LoggerFactory } from '../telemetry/LoggerFactory';
6+
7+
const log = LoggerFactory.getLogger('CodeLensHandler');
8+
9+
export function codeLensHandler(
10+
components: ServerComponents,
11+
): ServerRequestHandler<CodeLensParams, CodeLens[], never, void> {
12+
return (params, _token, _workDoneProgress, _resultProgress) => {
13+
log.debug({
14+
Handler: 'CodeLens',
15+
Document: params.textDocument.uri,
16+
});
17+
18+
const document = components.documents.documents.get(params.textDocument.uri);
19+
if (!document) {
20+
return [];
21+
}
22+
23+
const stackActions = getStackActionsCodeLenses(params.textDocument.uri);
24+
const managedResourceActions = components.managedResourceCodeLens.getCodeLenses(
25+
params.textDocument.uri,
26+
document,
27+
);
28+
29+
return [...stackActions, ...managedResourceActions];
30+
};
31+
}

src/handlers/ResourceHandler.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
import { randomUUID } from 'crypto';
12
import { ServerRequestHandler } from 'vscode-languageserver';
3+
import { RequestHandler } from 'vscode-languageserver/node';
4+
import { TopLevelSection } from '../context/ContextType';
5+
import { getEntityMap } from '../context/SectionContextBuilder';
26
import {
37
ResourceTypesResult,
48
ListResourcesParams,
@@ -12,6 +16,7 @@ import {
1216
} from '../resourceState/ResourceStateTypes';
1317
import { ResourceStackManagementResult } from '../resourceState/StackManagementInfoProvider';
1418
import { ServerComponents } from '../server/ServerComponents';
19+
import { GetStackTemplateParams, GetStackTemplateResult } from '../stacks/StackRequestType';
1520
import { LoggerFactory } from '../telemetry/LoggerFactory';
1621
import { extractErrorMessage } from '../utils/Errors';
1722

@@ -96,3 +101,63 @@ export function getStackMgmtInfo(
96101
return await components.stackManagementInfoProvider.getResourceManagementState(id);
97102
};
98103
}
104+
105+
export function getManagedResourceStackTemplateHandler(
106+
components: ServerComponents,
107+
): RequestHandler<GetStackTemplateParams, GetStackTemplateResult | undefined, void> {
108+
return async (params, _token) => {
109+
try {
110+
const template = await components.cfnService.getTemplate({ StackName: params.stackName });
111+
if (!template) {
112+
return;
113+
}
114+
115+
let lineNumber: number | undefined;
116+
117+
if (params.primaryIdentifier) {
118+
const resources = await components.cfnService.describeStackResources({ StackName: params.stackName });
119+
const resource = resources.StackResources?.find(
120+
(r) => r.PhysicalResourceId === params.primaryIdentifier,
121+
);
122+
123+
if (!resource?.LogicalResourceId) {
124+
throw new Error(
125+
`Resource with PhysicalResourceId ${params.primaryIdentifier} not found in stack ${params.stackName}`,
126+
);
127+
}
128+
129+
const logicalId = resource.LogicalResourceId;
130+
const tempUri = `temp://${randomUUID()}.template`;
131+
132+
try {
133+
components.syntaxTreeManager.add(tempUri, template);
134+
135+
const syntaxTree = components.syntaxTreeManager.getSyntaxTree(tempUri);
136+
if (syntaxTree) {
137+
const resourcesMap = getEntityMap(syntaxTree, TopLevelSection.Resources);
138+
const resourceContext = resourcesMap?.get(logicalId);
139+
if (resourceContext) {
140+
lineNumber = resourceContext.startPosition.row;
141+
}
142+
}
143+
} finally {
144+
components.syntaxTreeManager.deleteSyntaxTree(tempUri);
145+
}
146+
}
147+
148+
return {
149+
templateBody: template,
150+
lineNumber,
151+
};
152+
} catch (error) {
153+
log.error({
154+
Handler: 'GetManagedResourceStackTemplateHandler',
155+
StackName: params.stackName,
156+
ErrorMessage: error instanceof Error ? error.message : String(error),
157+
ErrorStack: error instanceof Error ? error.stack : undefined,
158+
Error: error,
159+
});
160+
throw error;
161+
}
162+
};
163+
}

src/protocol/LspHandlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
CodeActionParams,
2020
CodeAction,
2121
Command,
22+
CodeLens,
23+
CodeLensParams,
2224
} from 'vscode-languageserver/node';
2325
import {
2426
CompletionList,
@@ -129,4 +131,8 @@ export class LspHandlers {
129131
onDidChangeConfiguration(handler: NotificationHandler<DidChangeConfigurationParams>) {
130132
this.connection.onDidChangeConfiguration(handler);
131133
}
134+
135+
onCodeLens(handler: ServerRequestHandler<CodeLensParams, CodeLens[], never, void>) {
136+
this.connection.onCodeLens(handler);
137+
}
132138
}

src/protocol/LspStackHandlers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@ import {
1515
GetParametersResult,
1616
GetCapabilitiesResult,
1717
} from '../stacks/actions/StackActionRequestType';
18-
import { ListStacksParams, ListStacksRequest, ListStacksResult } from '../stacks/StackRequestType';
18+
import {
19+
ListStacksParams,
20+
ListStacksResult,
21+
ListStacksRequest,
22+
GetStackTemplateParams,
23+
GetStackTemplateResult,
24+
GetStackTemplateRequest,
25+
} from '../stacks/StackRequestType';
1926
import { Identifiable } from './LspTypes';
2027

2128
export class LspStackHandlers {
@@ -48,4 +55,8 @@ export class LspStackHandlers {
4855
onListStacks(handler: RequestHandler<ListStacksParams, ListStacksResult, void>) {
4956
this.connection.onRequest(ListStacksRequest.method, handler);
5057
}
58+
59+
onGetStackTemplate(handler: RequestHandler<GetStackTemplateParams, GetStackTemplateResult | undefined, void>) {
60+
this.connection.onRequest(GetStackTemplateRequest.method, handler);
61+
}
5162
}

src/server/CfnServer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ssoTokenChangedHandler,
99
} from '../handlers/AuthHandler';
1010
import { codeActionHandler } from '../handlers/CodeActionHandler';
11+
import { codeLensHandler } from '../handlers/CodeLensHandler';
1112
import { completionHandler } from '../handlers/CompletionHandler';
1213
import { configurationHandler } from '../handlers/ConfigurationHandler';
1314
import { definitionHandler } from '../handlers/DefinitionHandler';
@@ -18,6 +19,7 @@ import { hoverHandler } from '../handlers/HoverHandler';
1819
import { initializedHandler } from '../handlers/Initialize';
1920
import { inlineCompletionHandler } from '../handlers/InlineCompletionHandler';
2021
import {
22+
getManagedResourceStackTemplateHandler,
2123
listResourcesHandler,
2224
getResourceTypesHandler,
2325
importResourceStateHandler,
@@ -63,6 +65,7 @@ export class CfnServer {
6365
this.features.handlers.onDefinition(definitionHandler(this.components));
6466
this.features.handlers.onDocumentSymbol(documentSymbolHandler(this.components));
6567
this.features.handlers.onDidChangeConfiguration(configurationHandler(this.components));
68+
this.features.handlers.onCodeLens(codeLensHandler(this.components));
6669

6770
this.features.authHandlers.onIamCredentialsUpdate(iamCredentialsUpdateHandler(this.components));
6871
this.features.authHandlers.onBearerCredentialsUpdate(bearerCredentialsUpdateHandler(this.components));
@@ -77,6 +80,7 @@ export class CfnServer {
7780
this.features.stackHandlers.onGetValidationStatus(getValidationStatusHandler(this.components));
7881
this.features.stackHandlers.onGetDeploymentStatus(getDeploymentStatusHandler(this.components));
7982
this.features.stackHandlers.onListStacks(listStacksHandler(this.components));
83+
this.features.stackHandlers.onGetStackTemplate(getManagedResourceStackTemplateHandler(this.components));
8084

8185
this.features.resourceHandlers.onListResources(listResourcesHandler(this.components));
8286
this.features.resourceHandlers.onRefreshResourceList(refreshResourceListHandler(this.components));

src/server/ServerComponents.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { CfnAI } from '../ai/CfnAI';
22
import { AwsCredentials } from '../auth/AwsCredentials';
33
import { CompletionRouter } from '../autocomplete/CompletionRouter';
44
import { InlineCompletionRouter } from '../autocomplete/InlineCompletionRouter';
5+
import { ManagedResourceCodeLens } from '../codeLens/ManagedResourceCodeLens';
56
import { ContextManager } from '../context/ContextManager';
67
import { SyntaxTreeManager } from '../context/syntaxtree/SyntaxTreeManager';
78
import { DataStoreFactoryProvider, MultiDataStoreFactoryProvider } from '../datastore/DataStore';
@@ -92,6 +93,7 @@ export class ServerComponents {
9293
readonly definitionProvider: DefinitionProvider;
9394
readonly codeActionService: CodeActionService;
9495
readonly documentSymbolRouter: DocumentSymbolRouter;
96+
readonly managedResourceCodeLens: ManagedResourceCodeLens;
9597

9698
// AI
9799
readonly cfnAI: CfnAI;
@@ -145,6 +147,7 @@ export class ServerComponents {
145147
this.definitionProvider = overrides.definitionProvider ?? DefinitionProvider.create(this);
146148
this.codeActionService = overrides.codeActionService ?? CodeActionService.create(this);
147149
this.documentSymbolRouter = overrides.documentSymbolRouter ?? DocumentSymbolRouter.create(this);
150+
this.managedResourceCodeLens = overrides.managedResourceCodeLens ?? ManagedResourceCodeLens.create(this);
148151

149152
this.cfnAI = overrides.cfnAI ?? CfnAI.create(this);
150153

src/services/CfnService.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
waitUntilStackUpdateComplete,
4747
waitUntilStackCreateComplete,
4848
DescribeChangeSetCommandOutput,
49+
GetTemplateCommand,
4950
} from '@aws-sdk/client-cloudformation';
5051
import { WaiterConfiguration, WaiterResult } from '@smithy/util-waiter';
5152
import { ServerComponents } from '../server/ServerComponents';
@@ -107,6 +108,11 @@ export class CfnService {
107108
return await this.withClient((client) => client.send(new DescribeStacksCommand(params ?? {})));
108109
}
109110

111+
public async getTemplate(params: { StackName: string }): Promise<string | undefined> {
112+
const response = await this.withClient((client) => client.send(new GetTemplateCommand(params)));
113+
return response.TemplateBody;
114+
}
115+
110116
public async createChangeSet(params: {
111117
StackName: string;
112118
ChangeSetName: string;

src/stacks/StackRequestType.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,17 @@ export type ListStacksResult = {
1111
};
1212

1313
export const ListStacksRequest = new RequestType<ListStacksParams, ListStacksResult, void>('aws/cfn/stacks');
14+
15+
export type GetStackTemplateParams = {
16+
stackName: string;
17+
primaryIdentifier?: string;
18+
};
19+
20+
export type GetStackTemplateResult = {
21+
templateBody: string;
22+
lineNumber?: number;
23+
};
24+
25+
export const GetStackTemplateRequest = new RequestType<GetStackTemplateParams, GetStackTemplateResult, void>(
26+
'aws/cfn/stack/template',
27+
);

0 commit comments

Comments
 (0)