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
3 changes: 2 additions & 1 deletion src/autocomplete/ResourceStateCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { CfnValue } from '../context/semantic/SemanticTypes';
import { DocumentType } from '../document/Document';
import { DocumentManager } from '../document/DocumentManager';
import { ResourceStateManager } from '../resourceState/ResourceStateManager';
import { ResourceStatePurpose } from '../resourceState/ResourceStateTypes';
import { ResourceSchema } from '../schema/ResourceSchema';
import { SchemaRetriever } from '../schema/SchemaRetriever';
import { TransformersUtil } from '../schema/transformers/TransformersUtil';
Expand All @@ -25,7 +26,7 @@ import { createCompletionItem, handleSnippetJsonQuotes } from './CompletionUtils
const log = LoggerFactory.getLogger('ResourceStateCompletionProvider');

export class ResourceStateCompletionProvider implements CompletionProvider, Configurable, Closeable {
private readonly transformers = TransformersUtil.createTransformers();
private readonly transformers = TransformersUtil.createTransformers(ResourceStatePurpose.IMPORT);
private editorSettings: EditorSettings = DefaultSettings.editor;
private editorSettingsSubscription?: SettingsSubscription;

Expand Down
44 changes: 34 additions & 10 deletions src/resourceState/ResourceStateImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
ResourceIdentifier,
ResourceSelection,
ResourceStateParams,
ResourceStatePurpose,
ResourceStateResult,
ResourceTemplateFormat,
ResourceType,
Expand All @@ -30,7 +31,9 @@ interface ResourcesSection {
const log = LoggerFactory.getLogger('ResourceStateImporter');

export class ResourceStateImporter {
private readonly transformers = TransformersUtil.createTransformers();
private readonly importTransformers = TransformersUtil.createTransformers(ResourceStatePurpose.IMPORT);
private readonly cloneTransformers = TransformersUtil.createTransformers(ResourceStatePurpose.CLONE);

constructor(
private readonly documentManager: DocumentManager,
private readonly syntaxTreeManager: SyntaxTreeManager,
Expand All @@ -40,7 +43,7 @@ export class ResourceStateImporter {
) {}

public async importResourceState(params: ResourceStateParams): Promise<ResourceStateResult> {
const { resourceSelections, textDocument } = params;
const { resourceSelections, textDocument, purpose } = params;
if (!resourceSelections) {
return this.getFailureResponse('No resources selected for import.');
}
Expand All @@ -55,7 +58,11 @@ export class ResourceStateImporter {
return this.getFailureResponse('Import failed. Syntax tree not found');
}

const { fetchedResourceStates, importResult } = await this.getResourceStates(resourceSelections, syntaxTree);
const { fetchedResourceStates, importResult } = await this.getResourceStates(
resourceSelections,
syntaxTree,
purpose,
);
const resourceSection = this.getResourceSection(syntaxTree);
const insertPosition = this.getInsertPosition(resourceSection, document);
const docFormattedText = this.combineResourcesToDocumentFormat(
Expand Down Expand Up @@ -86,6 +93,7 @@ export class ResourceStateImporter {
private async getResourceStates(
resourceSelections: ResourceSelection[],
syntaxTree: SyntaxTree,
purpose: ResourceStatePurpose,
): Promise<{ fetchedResourceStates: ResourceTemplateFormat[]; importResult: ResourceStateResult }> {
const fetchedResourceStates: ResourceTemplateFormat[] = [];
const importResult: ResourceStateResult = {
Expand Down Expand Up @@ -120,11 +128,8 @@ export class ResourceStateImporter {
fetchedResourceStates.push({
[logicalId]: {
Type: resourceType,
Properties: this.applyTransformations(resourceState.properties, schema),
Metadata: {
PrimaryIdentifier: resourceIdentifier,
...(await this.getStackManagementMetadata(resourceIdentifier)),
},
Properties: this.applyTransformations(resourceState.properties, schema, purpose),
Metadata: await this.createMetadata(resourceIdentifier, purpose),
},
});
} else {
Expand Down Expand Up @@ -220,11 +225,17 @@ export class ResourceStateImporter {
}
}

private applyTransformations(properties: string, schema: ResourceSchema): Record<string, string> {
private applyTransformations(
properties: string,
schema: ResourceSchema,
purpose: ResourceStatePurpose,
): Record<string, string> {
const propertiesObj = JSON.parse(properties) as Record<string, string>;

if (schema) {
for (const transformer of this.transformers) {
const transformers =
purpose === ResourceStatePurpose.CLONE ? this.cloneTransformers : this.importTransformers;
for (const transformer of transformers) {
transformer.transform(propertiesObj, schema);
}
}
Expand Down Expand Up @@ -280,6 +291,19 @@ export class ResourceStateImporter {
};
}

private async createMetadata(resourceIdentifier: string, purpose?: ResourceStatePurpose) {
if (purpose === ResourceStatePurpose.CLONE) {
return {
PrimaryIdentifier: `<CLONE>${resourceIdentifier}`,
};
}

return {
PrimaryIdentifier: resourceIdentifier,
...(await this.getStackManagementMetadata(resourceIdentifier)),
};
}

private async getStackManagementMetadata(identifier: string) {
const stackManagementInfo = await this.stackManagementInfoProvider.getResourceManagementState(identifier);
return {
Expand Down
10 changes: 10 additions & 0 deletions src/resourceState/ResourceStateTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,18 @@ export type ResourceSelection = {
resourceIdentifiers: string[];
};

/*
* Import purpose is to move an existing resource to be managed by a stack
* Clone purpose is to create a new resource using the configuration of an existing resource as a reference
*/
export enum ResourceStatePurpose {
IMPORT = 'Import',
CLONE = 'Clone',
}

export interface ResourceStateParams extends CodeActionParams {
resourceSelections?: ResourceSelection[];
purpose: ResourceStatePurpose;
}

export interface ResourceStateResult extends CodeAction {
Expand Down
43 changes: 43 additions & 0 deletions src/schema/transformers/ReplacePrimaryIdentifierTransformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ResourceSchema } from '../ResourceSchema';
import { ResourceTemplateTransformer } from './ResourceTemplateTransformer';

export class ReplacePrimaryIdentifierTransformer implements ResourceTemplateTransformer {
private static readonly CLONE_PLACEHOLDER = '<CLONE INPUT REQUIRED>';

transform(resourceProperties: Record<string, unknown>, schema: ResourceSchema): void {
if (!schema.primaryIdentifier || schema.primaryIdentifier.length === 0) {
return;
}

for (const identifierPath of schema.primaryIdentifier) {
this.replacePrimaryIdentifierProperty(resourceProperties, identifierPath);
}
}

private replacePrimaryIdentifierProperty(properties: Record<string, unknown>, propertyPath: string): void {
// Handle nested property paths like "/properties/BucketName"
const pathParts = propertyPath.split('/').filter((part) => part !== '' && part !== 'properties');

if (pathParts.length === 0) {
return;
}

let current = properties;

// Navigate to the parent of the target property
for (let i = 0; i < pathParts.length - 1; i++) {
const part = pathParts[i];
if (current[part] && typeof current[part] === 'object') {
current = current[part] as Record<string, unknown>;
} else {
return; // Path doesn't exist
}
}

// Replace the final property with placeholder
const finalProperty = pathParts[pathParts.length - 1];
if (finalProperty in current) {
current[finalProperty] = ReplacePrimaryIdentifierTransformer.CLONE_PLACEHOLDER;
}
}
}
30 changes: 18 additions & 12 deletions src/schema/transformers/TransformersUtil.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import { ResourceStatePurpose } from '../../resourceState/ResourceStateTypes';
import { RemoveMutuallyExclusivePropertiesTransformer } from './RemoveMutuallyExclusivePropertiesTransformer';
import { RemoveReadonlyPropertiesTransformer } from './RemoveReadonlyPropertiesTransformer';
import { RemoveSystemTagsTransformer } from './RemoveSystemTagsTransformer';
import { ReplacePrimaryIdentifierTransformer } from './ReplacePrimaryIdentifierTransformer';
import type { ResourceTemplateTransformer } from './ResourceTemplateTransformer';

/**
* Utility class for managing resource template transformers
*/
export class TransformersUtil {
/**
* Creates an array of transformers for resource template processing
*/
public static createTransformers(): ResourceTemplateTransformer[] {
return [
new RemoveReadonlyPropertiesTransformer(),
new RemoveMutuallyExclusivePropertiesTransformer(),
new RemoveSystemTagsTransformer(),
];
public static createTransformers(purpose: ResourceStatePurpose): ResourceTemplateTransformer[] {
if (purpose === ResourceStatePurpose.IMPORT) {
return [
new RemoveReadonlyPropertiesTransformer(),
new RemoveMutuallyExclusivePropertiesTransformer(),
new RemoveSystemTagsTransformer(),
];
} else if (purpose === ResourceStatePurpose.CLONE) {
return [
new RemoveReadonlyPropertiesTransformer(),
new RemoveMutuallyExclusivePropertiesTransformer(),
new RemoveSystemTagsTransformer(),
new ReplacePrimaryIdentifierTransformer(),
];
}
return [];
}
}
Loading
Loading