Skip to content
Merged
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
118 changes: 118 additions & 0 deletions src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
export const deletionPolicyValueDocsMap: ReadonlyMap<string, string> = getDeletionPolicyValueDocsMap();

function getDeletionPolicyValueDocsMap(): Map<string, string> {
const docsMap = new Map<string, string>();

docsMap.set(
'Delete',
[
'**Delete**',
'\n',
'---',
'CloudFormation deletes the resource and all its content if applicable during stack deletion. ',
'You can add this deletion policy to any resource type. ',
"By default, if you don't specify a `DeletionPolicy`, CloudFormation deletes your resources. ",
'However, be aware of the following considerations:',
'\n',
'- For `AWS::RDS::DBCluster` resources, the default policy is `Snapshot`. ',
"- For `AWS::RDS::DBInstance` resources that don't specify the DBClusterIdentifier property, the default policy is `Snapshot`.",
'- For Amazon S3 buckets, you must delete all objects in the bucket for deletion to succeed. ',
'- The default behavior of CloudFormation is to delete the secret with the ForceDeleteWithoutRecovery flag. ',
'\n',
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
].join('\n'),
);

docsMap.set(
'Retain',
[
'**Retain**',
'\n',
'---',
'CloudFormation keeps the resource without deleting the resource or its contents when its stack is deleted. ',
'You can add this deletion policy to any resource type. ',
'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. ',
'\n',
'For update operations, the following considerations apply: ',
'\n',
"- If a resource is deleted, the `DeletionPolicy` retains the physical resource but ensures that it's deleted from CloudFormation's scope. ",
"- 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. ",
'\n',
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
].join('\n'),
);

docsMap.set(
'RetainExceptOnCreate',
[
'**RetainExceptOnCreate**',
'\n',
'---',
'`RetainExceptOnCreate` behaves like `Retain` for stack operations, except for the stack operation that initially created the resource.',
'If the stack operation that created the resource is rolled back, CloudFormation deletes the resource. ',
'For all other stack operations, such as stack deletion, CloudFormation retains the resource and its contents. ',
'The result is that new, empty, and unused resources are deleted, while in-use resources and their data are retained. ',
'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.',
'\n',
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
].join('\n'),
);

docsMap.set(
'Snapshot',
[
'**Snapshot**',
'\n',
'---',
'For resources that support snapshots, CloudFormation creates a snapshot for the resource before deleting it. ',
'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. ',
'Resources that support snapshots include: ',
'\n',
'- [AWS::DocDB::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-docdb-dbcluster.html)',
'- [AWS::EC2::Volume](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-ec2-volume.html)',
'- [AWS::ElastiCache::CacheCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-elasticache-cachecluster.html)',
'- [AWS::ElastiCache::ReplicationGroup](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-elasticache-replicationgroup.html)',
'- [AWS::Neptune::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-neptune-dbcluster.html)',
'- [AWS::RDS::DBCluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-rds-dbcluster.html)',
'- [AWS::RDS::DBInstance](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-rds-dbinstance.html)',
'- [AWS::Redshift::Cluster](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-redshift-cluster.html)',
'\n',
'[Source Documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-attribute-deletionpolicy.html)',
].join('\n'),
);

return docsMap;
}

export const SNAPSHOT_SUPPORTED_RESOURCE_TYPES: ReadonlyArray<string> = [
'AWS::DocDB::DBCluster',
'AWS::EC2::Volume',
'AWS::ElastiCache::CacheCluster',
'AWS::ElastiCache::ReplicationGroup',
'AWS::Neptune::DBCluster',
'AWS::RDS::DBCluster',
'AWS::RDS::DBInstance',
'AWS::Redshift::Cluster',
];

export const DEFAULT_SNAPSHOT_RESOURCE_TYPES: ReadonlyArray<string> = [
'AWS::RDS::DBCluster',
'AWS::RDS::DBInstance', // Only when DBClusterIdentifier is not specified
];

export const DELETION_POLICY_VALUES: ReadonlyArray<string> = ['Delete', 'Retain', 'RetainExceptOnCreate', 'Snapshot'];

export function supportsSnapshot(resourceType: string): boolean {
return SNAPSHOT_SUPPORTED_RESOURCE_TYPES.includes(resourceType);
}

export function getDefaultDeletionPolicy(resourceType: string): string {
if (DEFAULT_SNAPSHOT_RESOURCE_TYPES.includes(resourceType)) {
return 'Snapshot';
}
return 'Delete';
}

export function isValidDeletionPolicyValue(value: string): boolean {
return DELETION_POLICY_VALUES.includes(value);
}
21 changes: 21 additions & 0 deletions src/context/Context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,27 @@ export class Context {
return this.propertyPath.length > resourceAttributeIndex + 1;
}

public isResourceAttributeValue(): boolean {
if (this.section !== TopLevelSection.Resources || !this.hasLogicalId) {
return false;
}

if (this.propertyPath.length !== 3) {
return false;
}

const attributeName = this.propertyPath[2] as string;
if (!ResourceAttributesSet.has(attributeName)) {
return false;
}

if (this.text === attributeName) {
return false;
}

return this.isValue();
}

public getResourceAttributePropertyPath(): string[] {
const resourceAttributeIndex = this.propertyPath.findIndex((segment) =>
ResourceAttributesSet.has(segment as string),
Expand Down
17 changes: 17 additions & 0 deletions src/hover/HoverFormatter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/DeletionPolicyPropertyDocs';
import { ResourceAttribute } from '../context/ContextType';
import { Condition, Entity, Mapping, Parameter, Resource } from '../context/semantic/Entity';
import { EntityType } from '../context/semantic/SemanticTypes';
import { PropertyType } from '../schema/ResourceSchema';
Expand Down Expand Up @@ -1138,3 +1140,18 @@ export function formatResourceHover(resource: Resource): string {
const result = doc.filter((item) => item.trim() !== '').join('\n\n');
return result;
}

/**
* Gets documentation for resource attribute values based on the attribute type and text.
*/
export function getResourceAttributeValueDoc(attributeName: ResourceAttribute, text: string): string | undefined {
switch (attributeName) {
case ResourceAttribute.DeletionPolicy: {
return deletionPolicyValueDocsMap.get(text);
}
//TODO: add other ResourceAttribute Values as needed
default: {
return undefined;
}
}
}
26 changes: 23 additions & 3 deletions src/hover/IntrinsicFunctionArgumentHoverProvider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Context } from '../context/Context';
import { IntrinsicFunction } from '../context/ContextType';
import { IntrinsicFunction, ResourceAttribute, ResourceAttributesSet } from '../context/ContextType';
import { ContextWithRelatedEntities } from '../context/ContextWithRelatedEntities';
import { formatIntrinsicArgumentHover } from './HoverFormatter';
import { formatIntrinsicArgumentHover, getResourceAttributeValueDoc } from './HoverFormatter';
import { HoverProvider } from './HoverProvider';

export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
Expand All @@ -18,7 +18,11 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
return undefined;
}

// Handle different intrinsic function types
const resourceAttributeValueDoc = this.getResourceAttributeValueDoc(context);
if (resourceAttributeValueDoc) {
return resourceAttributeValueDoc;
}

switch (intrinsicFunction.type) {
case IntrinsicFunction.Ref: {
return this.handleRefArgument(context);
Expand Down Expand Up @@ -60,4 +64,20 @@ export class IntrinsicFunctionArgumentHoverProvider implements HoverProvider {
private buildSchemaAndFormat(relatedContext: Context): string | undefined {
return formatIntrinsicArgumentHover(relatedContext.entity);
}

/**
* Check if we're inside an intrinsic function that's providing a value for a resource attribute
* and return documentation for that value if applicable.
*/
private getResourceAttributeValueDoc(context: Context): string | undefined {
// Find the resource attribute in the property path
for (const pathSegment of context.propertyPath) {
if (ResourceAttributesSet.has(pathSegment as string)) {
const attributeName = pathSegment as ResourceAttribute;
return getResourceAttributeValueDoc(attributeName, context.text);
}
}

return undefined;
}
}
22 changes: 21 additions & 1 deletion src/hover/ResourceSectionHoverProvider.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { resourceAttributeDocsMap } from '../artifacts/ResourceAttributeDocs';
import { creationPolicyPropertyDocsMap } from '../artifacts/resourceAttributes/CreationPolicyPropertyDocs';
import { deletionPolicyValueDocsMap } from '../artifacts/resourceAttributes/DeletionPolicyPropertyDocs';
import { Context } from '../context/Context';
import { ResourceAttribute, TopLevelSection } from '../context/ContextType';
import { Resource } from '../context/semantic/Entity';
import { ResourceSchema } from '../schema/ResourceSchema';
import { SchemaRetriever } from '../schema/SchemaRetriever';
import { templatePathToJsonPointerPath } from '../utils/PathUtils';
import { propertyTypesToMarkdown, formatResourceHover } from './HoverFormatter';
import { propertyTypesToMarkdown, formatResourceHover, getResourceAttributeValueDoc } from './HoverFormatter';
import { HoverProvider } from './HoverProvider';

export class ResourceSectionHoverProvider implements HoverProvider {
Expand All @@ -33,6 +34,9 @@ export class ResourceSectionHoverProvider implements HoverProvider {
if (context.isResourceAttributeProperty()) {
return this.getResourceAttributePropertyDoc(context, resource);
}
if (context.isResourceAttributeValue()) {
return this.getResourceAttributeValueDoc(context);
}
if (context.isResourceAttribute && resource[context.text] !== undefined) {
return this.getResourceAttributeDoc(context.text);
}
Expand Down Expand Up @@ -104,6 +108,9 @@ export class ResourceSectionHoverProvider implements HoverProvider {
case ResourceAttribute.CreationPolicy: {
return this.getCreationPolicyPropertyDoc(propertyPath);
}
case ResourceAttribute.DeletionPolicy: {
return this.getDeletionPolicyPropertyDoc(propertyPath);
}
default: {
return undefined;
}
Expand All @@ -114,4 +121,17 @@ export class ResourceSectionHoverProvider implements HoverProvider {
const propertyPathString = propertyPath.join('.');
return creationPolicyPropertyDocsMap.get(propertyPathString);
}

private getDeletionPolicyPropertyDoc(propertyPath: ReadonlyArray<string>): string | undefined {
if (propertyPath.length === 2) {
const deletionPolicyValue = propertyPath[1];
return deletionPolicyValueDocsMap.get(deletionPolicyValue);
}
return undefined;
}

private getResourceAttributeValueDoc(context: Context): string | undefined {
const attributeName = context.propertyPath[2] as ResourceAttribute;
return getResourceAttributeValueDoc(attributeName, context.text);
}
}
25 changes: 25 additions & 0 deletions tst/e2e/hover/Hover.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { parameterAttributeDocsMap } from '../../../src/artifacts/ParameterAttri
import { pseudoParameterDocsMap } from '../../../src/artifacts/PseudoParameterDocs';
import { resourceAttributeDocsMap } from '../../../src/artifacts/ResourceAttributeDocs';
import { creationPolicyPropertyDocsMap } from '../../../src/artifacts/resourceAttributes/CreationPolicyPropertyDocs';
import { deletionPolicyValueDocsMap } from '../../../src/artifacts/resourceAttributes/DeletionPolicyPropertyDocs';
import { templateSectionDocsMap } from '../../../src/artifacts/TemplateSectionDocs';
import {
TopLevelSection,
Expand Down Expand Up @@ -1265,6 +1266,30 @@ Resources:`,
.build(),
},
},
{
action: 'type',
content: ``,
position: { line: 302, character: 31 },
description: 'Hover Snapshot in DeletionPolicy Resource Attribute',
verification: {
position: { line: 286, character: 44 },
expectation: HoverExpectationBuilder.create()
.expectContent(deletionPolicyValueDocsMap.get('Snapshot'))
.build(),
},
},
{
action: 'type',
content: ``,
position: { line: 302, character: 31 },
description: 'Hover Delete in DeletionPolicy Resource Attribute',
verification: {
position: { line: 286, character: 52 },
expectation: HoverExpectationBuilder.create()
.expectContent(deletionPolicyValueDocsMap.get('Delete'))
.build(),
},
},
{
action: 'type',
content: `
Expand Down
28 changes: 28 additions & 0 deletions tst/unit/context/Context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,34 @@ describe('Context', () => {
});
});

describe('isResourceAttributeValue method', () => {
it('should return true when positioned at resource attribute value', () => {
const context = getContextAt(94, 20); // Position at "Retain" in "DeletionPolicy: Retain"

expect(context).toBeDefined();
expect(context!.section).toBe(TopLevelSection.Resources);
expect(context!.hasLogicalId).toBe(true);
expect(context!.isResourceAttributeValue()).toBe(true);
});

it('should return false when positioned at resource attribute key', () => {
const context = getContextAt(94, 4); // Position at "DeletionPolicy:"

expect(context).toBeDefined();
expect(context!.section).toBe(TopLevelSection.Resources);
expect(context!.text).toBe('DeletionPolicy');
expect(context!.isResourceAttributeValue()).toBe(false);
});

it('should return false when not in Resources section', () => {
const context = getContextAt(21, 4); // Position at "EnvironmentType:" in Parameters section

expect(context).toBeDefined();
expect(context!.section).toBe(TopLevelSection.Parameters);
expect(context!.isResourceAttributeValue()).toBe(false);
});
});

describe('Comprehensive Resource Entity with All Attributes', () => {
it('should create comprehensive resource entity with all resource attributes', () => {
const context = getContextAt(88, 4); // ComprehensiveResource
Expand Down
Loading
Loading