Skip to content

Commit 7aa5645

Browse files
tylerymarkowitzkddejong
authored andcommitted
add hover for deletion policy resource attribute
1 parent aaf66c1 commit 7aa5645

File tree

9 files changed

+505
-4
lines changed

9 files changed

+505
-4
lines changed
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
export const deletionPolicyValueDocsMap: ReadonlyMap<string, string> = getDeletionPolicyValueDocsMap();
2+
3+
function getDeletionPolicyValueDocsMap(): Map<string, string> {
4+
const docsMap = new Map<string, string>();
5+
6+
docsMap.set(
7+
'Delete',
8+
[
9+
'**Delete**',
10+
'\n',
11+
'---',
12+
'CloudFormation deletes the resource and all its content if applicable during stack deletion. ',
13+
'You can add this deletion policy to any resource type. ',
14+
"By default, if you don't specify a `DeletionPolicy`, CloudFormation deletes your resources. ",
15+
'However, be aware of the following considerations:',
16+
'\n',
17+
'- For `AWS::RDS::DBCluster` resources, the default policy is `Snapshot`. ',
18+
"- For `AWS::RDS::DBInstance` resources that don't specify the DBClusterIdentifier property, the default policy is `Snapshot`.",
19+
'- For Amazon S3 buckets, you must delete all objects in the bucket for deletion to succeed. ',
20+
'- The default behavior of CloudFormation is to delete the secret with the ForceDeleteWithoutRecovery flag. ',
21+
'\n',
22+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
23+
].join('\n'),
24+
);
25+
26+
docsMap.set(
27+
'Retain',
28+
[
29+
'**Retain**',
30+
'\n',
31+
'---',
32+
'CloudFormation keeps the resource without deleting the resource or its contents when its stack is deleted. ',
33+
'You can add this deletion policy to any resource type. ',
34+
'When CloudFormation completes the stack deletion, the stack will be in `Delete_Complete` state; however, resources that are retained continue to exist and continue to incur applicable charges until you delete those resources. ',
35+
'\n',
36+
'For update operations, the following considerations apply: ',
37+
'\n',
38+
"- If a resource is deleted, the `DeletionPolicy` retains the physical resource but ensures that it's deleted from CloudFormation's scope. ",
39+
"- If a resource is updated such that a new physical resource is created to replace the old resource, then the old resource is completely deleted, including from CloudFormation's scope. ",
40+
'\n',
41+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
42+
].join('\n'),
43+
);
44+
45+
docsMap.set(
46+
'RetainExceptOnCreate',
47+
[
48+
'**RetainExceptOnCreate**',
49+
'\n',
50+
'---',
51+
'`RetainExceptOnCreate` behaves like `Retain` for stack operations, except for the stack operation that initially created the resource.',
52+
'If the stack operation that created the resource is rolled back, CloudFormation deletes the resource. ',
53+
'For all other stack operations, such as stack deletion, CloudFormation retains the resource and its contents. ',
54+
'The result is that new, empty, and unused resources are deleted, while in-use resources and their data are retained. ',
55+
'Refer to the [UpdateStack](https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_UpdateStack.html) API documentation to use this deletion policy as an API parameter without updating your template.',
56+
'\n',
57+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
58+
].join('\n'),
59+
);
60+
61+
docsMap.set(
62+
'Snapshot',
63+
[
64+
'**Snapshot**',
65+
'\n',
66+
'---',
67+
'For resources that support snapshots, CloudFormation creates a snapshot for the resource before deleting it. ',
68+
'When CloudFormation completes the stack deletion, the stack will be in the `Delete_Complete` state; however, the snapshots that are created with this policy continue to exist and continue to incur applicable charges until you delete those snapshots. ',
69+
'Resources that support snapshots include: ',
70+
'\n',
71+
'- [AWS::DocDB::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-docdb-dbcluster.html)',
72+
'- [AWS::EC2::Volume](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-ec2-volume.html)',
73+
'- [AWS::ElastiCache::CacheCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-elasticache-cachecluster.html)',
74+
'- [AWS::ElastiCache::ReplicationGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-elasticache-replicationgroup.html)',
75+
'- [AWS::Neptune::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-neptune-dbcluster.html)',
76+
'- [AWS::RDS::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-rds-dbcluster.html)',
77+
'- [AWS::RDS::DBInstance](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-rds-dbinstance.html)',
78+
'- [AWS::Redshift::Cluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-redshift-cluster.html)',
79+
'\n',
80+
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
81+
].join('\n'),
82+
);
83+
84+
return docsMap;
85+
}
86+
87+
export const SNAPSHOT_SUPPORTED_RESOURCE_TYPES: ReadonlyArray<string> = [
88+
'AWS::DocDB::DBCluster',
89+
'AWS::EC2::Volume',
90+
'AWS::ElastiCache::CacheCluster',
91+
'AWS::ElastiCache::ReplicationGroup',
92+
'AWS::Neptune::DBCluster',
93+
'AWS::RDS::DBCluster',
94+
'AWS::RDS::DBInstance',
95+
'AWS::Redshift::Cluster',
96+
];
97+
98+
export const DEFAULT_SNAPSHOT_RESOURCE_TYPES: ReadonlyArray<string> = [
99+
'AWS::RDS::DBCluster',
100+
'AWS::RDS::DBInstance', // Only when DBClusterIdentifier is not specified
101+
];
102+
103+
export const DELETION_POLICY_VALUES: ReadonlyArray<string> = ['Delete', 'Retain', 'RetainExceptOnCreate', 'Snapshot'];
104+
105+
export function supportsSnapshot(resourceType: string): boolean {
106+
return SNAPSHOT_SUPPORTED_RESOURCE_TYPES.includes(resourceType);
107+
}
108+
109+
export function getDefaultDeletionPolicy(resourceType: string): string {
110+
if (DEFAULT_SNAPSHOT_RESOURCE_TYPES.includes(resourceType)) {
111+
return 'Snapshot';
112+
}
113+
return 'Delete';
114+
}
115+
116+
export function isValidDeletionPolicyValue(value: string): boolean {
117+
return DELETION_POLICY_VALUES.includes(value);
118+
}

src/context/Context.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,27 @@ export class Context {
142142
return this.propertyPath.length > resourceAttributeIndex + 1;
143143
}
144144

145+
public isResourceAttributeValue(): boolean {
146+
if (this.section !== TopLevelSection.Resources || !this.hasLogicalId) {
147+
return false;
148+
}
149+
150+
if (this.propertyPath.length !== 3) {
151+
return false;
152+
}
153+
154+
const attributeName = this.propertyPath[2] as string;
155+
if (!ResourceAttributesSet.has(attributeName)) {
156+
return false;
157+
}
158+
159+
if (this.text === attributeName) {
160+
return false;
161+
}
162+
163+
return this.isValue();
164+
}
165+
145166
public getResourceAttributePropertyPath(): string[] {
146167
const resourceAttributeIndex = this.propertyPath.findIndex((segment) =>
147168
ResourceAttributesSet.has(segment as string),

src/hover/HoverFormatter.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/DeletionPolicyPropertyDocs';
2+
import { ResourceAttribute } from '../context/ContextType';
13
import { Condition, Entity, Mapping, Parameter, Resource } from '../context/semantic/Entity';
24
import { EntityType } from '../context/semantic/SemanticTypes';
35
import { PropertyType } from '../schema/ResourceSchema';
@@ -1138,3 +1140,18 @@ export function formatResourceHover(resource: Resource): string {
11381140
const result = doc.filter((item) => item.trim() !== '').join('\n\n');
11391141
return result;
11401142
}
1143+
1144+
/**
1145+
* Gets documentation for resource attribute values based on the attribute type and text.
1146+
*/
1147+
export function getResourceAttributeValueDoc(attributeName: ResourceAttribute, text: string): string | undefined {
1148+
switch (attributeName) {
1149+
case ResourceAttribute.DeletionPolicy: {
1150+
return deletionPolicyValueDocsMap.get(text);
1151+
}
1152+
//TODO: add other ResourceAttribute Values as needed
1153+
default: {
1154+
return undefined;
1155+
}
1156+
}
1157+
}

src/hover/IntrinsicFunctionArgumentHoverProvider.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Context } from '../context/Context';
2-
import { IntrinsicFunction } from '../context/ContextType';
2+
import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet } from '../context/ContextType';
33
import { ContextWithRelatedEntities } from '../context/ContextWithRelatedEntities';
4-
import { formatIntrinsicArgumentHover } from './HoverFormatter';
4+
import { formatIntrinsicArgumentHover, getResourceAttributeValueDoc } from './HoverFormatter';
55
import { HoverProvider } from './HoverProvider';
66

77
export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
@@ -18,7 +18,11 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
1818
return undefined;
1919
}
2020

21-
// Handle different intrinsic function types
21+
const resourceAttributeValueDoc = this.getResourceAttributeValueDoc(context);
22+
if (resourceAttributeValueDoc) {
23+
return resourceAttributeValueDoc;
24+
}
25+
2226
switch (intrinsicFunction.type) {
2327
case IntrinsicFunction.Ref: {
2428
return this.handleRefArgument(context);
@@ -60,4 +64,20 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
6064
private buildSchemaAndFormat(relatedContext: Context): string | undefined {
6165
return formatIntrinsicArgumentHover(relatedContext.entity);
6266
}
67+
68+
/**
69+
* Check if we're inside an intrinsic function that's providing a value for a resource attribute
70+
* and return documentation for that value if applicable.
71+
*/
72+
private getResourceAttributeValueDoc(context: Context): string | undefined {
73+
// Find the resource attribute in the property path
74+
for (const pathSegment of context.propertyPath) {
75+
if (ResourceAttributesSet.has(pathSegment as string)) {
76+
const attributeName = pathSegment as ResourceAttribute;
77+
return getResourceAttributeValueDoc(attributeName, context.text);
78+
}
79+
}
80+
81+
return undefined;
82+
}
6383
}

src/hover/ResourceSectionHoverProvider.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
import { resourceAttributeDocsMap } from '../artifacts/ResourceAttributeDocs';
22
import { creationPolicyPropertyDocsMap } from '../artifacts/resourceAttributes/CreationPolicyPropertyDocs';
3+
import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/DeletionPolicyPropertyDocs';
34
import { Context } from '../context/Context';
45
import { ResourceAttribute, TopLevelSection } from '../context/ContextType';
56
import { Resource } from '../context/semantic/Entity';
67
import { ResourceSchema } from '../schema/ResourceSchema';
78
import { SchemaRetriever } from '../schema/SchemaRetriever';
89
import { templatePathToJsonPointerPath } from '../utils/PathUtils';
9-
import { propertyTypesToMarkdown, formatResourceHover } from './HoverFormatter';
10+
import { propertyTypesToMarkdown, formatResourceHover, getResourceAttributeValueDoc } from './HoverFormatter';
1011
import { HoverProvider } from './HoverProvider';
1112

1213
export class ResourceSectionHoverProvider implements HoverProvider {
@@ -33,6 +34,9 @@ export class ResourceSectionHoverProvider implements HoverProvider {
3334
if (context.isResourceAttributeProperty()) {
3435
return this.getResourceAttributePropertyDoc(context, resource);
3536
}
37+
if (context.isResourceAttributeValue()) {
38+
return this.getResourceAttributeValueDoc(context);
39+
}
3640
if (context.isResourceAttribute && resource[context.text] !== undefined) {
3741
return this.getResourceAttributeDoc(context.text);
3842
}
@@ -104,6 +108,9 @@ export class ResourceSectionHoverProvider implements HoverProvider {
104108
case ResourceAttribute.CreationPolicy: {
105109
return this.getCreationPolicyPropertyDoc(propertyPath);
106110
}
111+
case ResourceAttribute.DeletionPolicy: {
112+
return this.getDeletionPolicyPropertyDoc(propertyPath);
113+
}
107114
default: {
108115
return undefined;
109116
}
@@ -114,4 +121,17 @@ export class ResourceSectionHoverProvider implements HoverProvider {
114121
const propertyPathString = propertyPath.join('.');
115122
return creationPolicyPropertyDocsMap.get(propertyPathString);
116123
}
124+
125+
private getDeletionPolicyPropertyDoc(propertyPath: ReadonlyArray<string>): string | undefined {
126+
if (propertyPath.length === 2) {
127+
const deletionPolicyValue = propertyPath[1];
128+
return deletionPolicyValueDocsMap.get(deletionPolicyValue);
129+
}
130+
return undefined;
131+
}
132+
133+
private getResourceAttributeValueDoc(context: Context): string | undefined {
134+
const attributeName = context.propertyPath[2] as ResourceAttribute;
135+
return getResourceAttributeValueDoc(attributeName, context.text);
136+
}
117137
}

tst/e2e/hover/Hover.test.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { parameterAttributeDocsMap } from '../../../src/artifacts/ParameterAttri
55
import { pseudoParameterDocsMap } from '../../../src/artifacts/PseudoParameterDocs';
66
import { resourceAttributeDocsMap } from '../../../src/artifacts/ResourceAttributeDocs';
77
import { creationPolicyPropertyDocsMap } from '../../../src/artifacts/resourceAttributes/CreationPolicyPropertyDocs';
8+
import { deletionPolicyValueDocsMap } from '../../../src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs';
89
import { templateSectionDocsMap } from '../../../src/artifacts/TemplateSectionDocs';
910
import {
1011
TopLevelSection,
@@ -1265,6 +1266,30 @@ Resources:`,
12651266
.build(),
12661267
},
12671268
},
1269+
{
1270+
action: 'type',
1271+
content: ``,
1272+
position: { line: 302, character: 31 },
1273+
description: 'Hover Snapshot in DeletionPolicy Resource Attribute',
1274+
verification: {
1275+
position: { line: 286, character: 44 },
1276+
expectation: HoverExpectationBuilder.create()
1277+
.expectContent(deletionPolicyValueDocsMap.get('Snapshot'))
1278+
.build(),
1279+
},
1280+
},
1281+
{
1282+
action: 'type',
1283+
content: ``,
1284+
position: { line: 302, character: 31 },
1285+
description: 'Hover Delete in DeletionPolicy Resource Attribute',
1286+
verification: {
1287+
position: { line: 286, character: 52 },
1288+
expectation: HoverExpectationBuilder.create()
1289+
.expectContent(deletionPolicyValueDocsMap.get('Delete'))
1290+
.build(),
1291+
},
1292+
},
12681293
{
12691294
action: 'type',
12701295
content: `

tst/unit/context/Context.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,34 @@ describe('Context', () => {
286286
});
287287
});
288288

289+
describe('isResourceAttributeValue method', () => {
290+
it('should return true when positioned at resource attribute value', () => {
291+
const context = getContextAt(94, 20); // Position at "Retain" in "DeletionPolicy: Retain"
292+
293+
expect(context).toBeDefined();
294+
expect(context!.section).toBe(TopLevelSection.Resources);
295+
expect(context!.hasLogicalId).toBe(true);
296+
expect(context!.isResourceAttributeValue()).toBe(true);
297+
});
298+
299+
it('should return false when positioned at resource attribute key', () => {
300+
const context = getContextAt(94, 4); // Position at "DeletionPolicy:"
301+
302+
expect(context).toBeDefined();
303+
expect(context!.section).toBe(TopLevelSection.Resources);
304+
expect(context!.text).toBe('DeletionPolicy');
305+
expect(context!.isResourceAttributeValue()).toBe(false);
306+
});
307+
308+
it('should return false when not in Resources section', () => {
309+
const context = getContextAt(21, 4); // Position at "EnvironmentType:" in Parameters section
310+
311+
expect(context).toBeDefined();
312+
expect(context!.section).toBe(TopLevelSection.Parameters);
313+
expect(context!.isResourceAttributeValue()).toBe(false);
314+
});
315+
});
316+
289317
describe('Comprehensive Resource Entity with All Attributes', () => {
290318
it('should create comprehensive resource entity with all resource attributes', () => {
291319
const context = getContextAt(88, 4); // ComprehensiveResource

0 commit comments

Comments
 (0)