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
32 changes: 32 additions & 0 deletions src/handlers/StackHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
CreateStackActionParams,
CreateStackActionResult,
GetStackActionStatusResult,
DescribeValidationStatusResult,
DescribeDeploymentStatusResult,
} from '../stacks/actions/StackActionRequestType';
import { ListStacksParams, ListStacksResult } from '../stacks/StackRequestType';
import { LoggerFactory } from '../telemetry/LoggerFactory';
Expand Down Expand Up @@ -110,6 +112,36 @@ export function getDeploymentStatusHandler(
};
}

export function describeValidationStatusHandler(
components: ServerComponents,
): RequestHandler<Identifiable, DescribeValidationStatusResult, void> {
return (rawParams) => {
log.debug({ Handler: 'describeValidationStatusHandler', rawParams });

try {
const params = parseWithPrettyError(parseIdentifiable, rawParams);
return components.validationWorkflowService.describeStatus(params);
} catch (error) {
handleStackActionError(error, 'Failed to describe validation status');
}
};
}

export function describeDeploymentStatusHandler(
components: ServerComponents,
): RequestHandler<Identifiable, DescribeDeploymentStatusResult, void> {
return (rawParams) => {
log.debug({ Handler: 'describeDeploymentStatusHandler', rawParams });

try {
const params = parseWithPrettyError(parseIdentifiable, rawParams);
return components.deploymentWorkflowService.describeStatus(params);
} catch (error) {
handleStackActionError(error, 'Failed to describe deployment status');
}
};
}

export function getCapabilitiesHandler(
components: ServerComponents,
): RequestHandler<TemplateUri, GetCapabilitiesResult, void> {
Expand Down
12 changes: 12 additions & 0 deletions src/protocol/LspStackHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
GetValidationStatusRequest,
GetCapabilitiesRequest,
GetParametersRequest,
DescribeValidationStatusRequest,
DescribeDeploymentStatusRequest,
} from '../stacks/actions/StackActionProtocol';
import {
TemplateUri,
Expand All @@ -14,6 +16,8 @@ import {
GetStackActionStatusResult,
GetParametersResult,
GetCapabilitiesResult,
DescribeValidationStatusResult,
DescribeDeploymentStatusResult,
} from '../stacks/actions/StackActionRequestType';
import {
ListStacksParams,
Expand Down Expand Up @@ -44,6 +48,14 @@ export class LspStackHandlers {
this.connection.onRequest(GetDeploymentStatusRequest.method, handler);
}

onDescribeValidationStatus(handler: RequestHandler<Identifiable, DescribeValidationStatusResult, void>) {
this.connection.onRequest(DescribeValidationStatusRequest.method, handler);
}

onDescribeDeploymentStatus(handler: RequestHandler<Identifiable, DescribeDeploymentStatusResult, void>) {
this.connection.onRequest(DescribeDeploymentStatusRequest.method, handler);
}

onGetParameters(handler: RequestHandler<TemplateUri, GetParametersResult, void>) {
this.connection.onRequest(GetParametersRequest.method, handler);
}
Expand Down
9 changes: 7 additions & 2 deletions src/server/CfnLspProviders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import { ResourceStateManager } from '../resourceState/ResourceStateManager';
import { StackManagementInfoProvider } from '../resourceState/StackManagementInfoProvider';
import { CodeActionService } from '../services/CodeActionService';
import { DeploymentWorkflow } from '../stacks/actions/DeploymentWorkflow';
import {
DescribeDeploymentStatusResult,
DescribeValidationStatusResult,
} from '../stacks/actions/StackActionRequestType';
import { StackActionWorkflow } from '../stacks/actions/StackActionWorkflowType';
import { ValidationWorkflow } from '../stacks/actions/ValidationWorkflow';
import { Closeable, closeSafely } from '../utils/Closeable';
import { Configurable, Configurables } from '../utils/Configurable';
Expand All @@ -19,8 +24,8 @@ import { CfnInfraCore } from './CfnInfraCore';
export class CfnLspProviders implements Configurables, Closeable {
// Business logic
readonly stackManagementInfoProvider: StackManagementInfoProvider;
readonly validationWorkflowService: ValidationWorkflow;
readonly deploymentWorkflowService: DeploymentWorkflow;
readonly validationWorkflowService: StackActionWorkflow<DescribeValidationStatusResult>;
readonly deploymentWorkflowService: StackActionWorkflow<DescribeDeploymentStatusResult>;
readonly resourceStateManager: ResourceStateManager;
readonly resourceStateImporter: ResourceStateImporter;

Expand Down
4 changes: 4 additions & 0 deletions src/server/CfnServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import {
getDeploymentStatusHandler,
getParametersHandler,
getCapabilitiesHandler,
describeValidationStatusHandler,
describeDeploymentStatusHandler,
} from '../handlers/StackHandler';
import { LspComponents } from '../protocol/LspComponents';
import { closeSafely } from '../utils/Closeable';
Expand Down Expand Up @@ -101,6 +103,8 @@ export class CfnServer {
this.lsp.stackHandlers.onCreateDeployment(createDeploymentHandler(this.components));
this.lsp.stackHandlers.onGetValidationStatus(getValidationStatusHandler(this.components));
this.lsp.stackHandlers.onGetDeploymentStatus(getDeploymentStatusHandler(this.components));
this.lsp.stackHandlers.onDescribeValidationStatus(describeValidationStatusHandler(this.components));
this.lsp.stackHandlers.onDescribeDeploymentStatus(describeDeploymentStatusHandler(this.components));
this.lsp.stackHandlers.onListStacks(listStacksHandler(this.components));
this.lsp.stackHandlers.onGetStackTemplate(getManagedResourceStackTemplateHandler(this.components));

Expand Down
1 change: 1 addition & 0 deletions src/services/CfnService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export class CfnService {
public async executeChangeSet(params: {
ChangeSetName: string;
StackName?: string;
ClientRequestToken: string;
}): Promise<ExecuteChangeSetCommandOutput> {
return await this.withClient((client) => client.send(new ExecuteChangeSetCommand(params)));
}
Expand Down
133 changes: 110 additions & 23 deletions src/stacks/actions/DeploymentWorkflow.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,31 @@
import { ChangeSetType } from '@aws-sdk/client-cloudformation';
import { DateTime } from 'luxon';
import { DocumentManager } from '../../document/DocumentManager';
import { Identifiable } from '../../protocol/LspTypes';
import { CfnExternal } from '../../server/CfnExternal';
import { CfnInfraCore } from '../../server/CfnInfraCore';
import { CfnService } from '../../services/CfnService';
import { LoggerFactory } from '../../telemetry/LoggerFactory';
import { processChangeSet, waitForValidation, waitForDeployment } from './StackActionOperations';
import { extractErrorMessage } from '../../utils/Errors';
import {
processChangeSet,
waitForValidation,
waitForDeployment,
processWorkflowUpdates,
} from './StackActionOperations';
import {
CreateStackActionParams,
CreateStackActionResult,
StackActionPhase,
StackActionState,
GetStackActionStatusResult,
DescribeDeploymentStatusResult,
DeploymentEvent,
} from './StackActionRequestType';
import { StackActionWorkflowState, StackActionWorkflow } from './StackActionWorkflowType';
import { DRY_RUN_VALIDATION_NAME } from './ValidationWorkflow';

export class DeploymentWorkflow implements StackActionWorkflow {
export class DeploymentWorkflow implements StackActionWorkflow<DescribeDeploymentStatusResult> {
private readonly workflows = new Map<string, StackActionWorkflowState>();
private readonly log = LoggerFactory.getLogger(DeploymentWorkflow);

Expand Down Expand Up @@ -69,14 +79,28 @@ export class DeploymentWorkflow implements StackActionWorkflow {
};
}

private async runDeploymentAsync(
describeStatus(params: Identifiable): DescribeDeploymentStatusResult {
const workflow = this.workflows.get(params.id);
if (!workflow) {
throw new Error(`Workflow not found: ${params.id}`);
}

return {
...this.getStatus(params),
ValidationDetails: workflow.validationDetails,
DeploymentEvents: workflow.deploymentEvents,
FailureReason: workflow.failureReason,
};
}

protected async runDeploymentAsync(
workflowId: string,
changeSetName: string,
stackName: string,
changeSetType: ChangeSetType,
): Promise<void> {
let validationResult;
const existingWorkflow = this.workflows.get(workflowId);
let existingWorkflow = this.workflows.get(workflowId);
if (!existingWorkflow) {
this.log.error({ workflowId }, 'Workflow not found during async execution');
return;
Expand All @@ -85,52 +109,115 @@ export class DeploymentWorkflow implements StackActionWorkflow {
try {
validationResult = await waitForValidation(this.cfnService, changeSetName, stackName);

this.workflows.set(workflowId, {
...existingWorkflow,
existingWorkflow = processWorkflowUpdates(this.workflows, existingWorkflow, {
phase: validationResult.phase,
state: validationResult.state,
changes: validationResult.changes,
});

if (validationResult.state === StackActionState.FAILED) {
existingWorkflow = processWorkflowUpdates(this.workflows, existingWorkflow, {
state: StackActionState.FAILED,
validationDetails: [
{
ValidationName: DRY_RUN_VALIDATION_NAME,
Timestamp: DateTime.now(),
Severity: 'ERROR',
Message: `Validation failed with reason: ${validationResult.failureReason}`,
},
],
});

return;
} else {
existingWorkflow = processWorkflowUpdates(this.workflows, existingWorkflow, {
validationDetails: [
{
ValidationName: DRY_RUN_VALIDATION_NAME,
Timestamp: DateTime.now(),
Severity: 'INFO',
Message: 'Validation succeeded',
},
],
});
}

this.workflows.set(workflowId, {
...existingWorkflow,
phase: StackActionPhase.VALIDATION_COMPLETE,
state: StackActionState.IN_PROGRESS,
changes: validationResult.changes,
} catch (error) {
this.log.error({ error, workflowId }, 'Deployment workflow threw exception during validation phase');
processWorkflowUpdates(this.workflows, existingWorkflow, {
phase: StackActionPhase.VALIDATION_FAILED,
state: StackActionState.FAILED,
failureReason: extractErrorMessage(error),
});

return;
}

try {
await this.cfnService.executeChangeSet({
StackName: stackName,
ChangeSetName: changeSetName,
ClientRequestToken: workflowId,
});
} catch (error) {
this.log.error({ error, workflowId }, 'Deployment workflow threw exception during deployment start phase');
processWorkflowUpdates(this.workflows, existingWorkflow, {
phase: StackActionPhase.DEPLOYMENT_FAILED,
state: StackActionState.FAILED,
failureReason: extractErrorMessage(error),
});

return;
}

this.workflows.set(workflowId, {
try {
existingWorkflow = processWorkflowUpdates(this.workflows, existingWorkflow, {
...existingWorkflow,
phase: StackActionPhase.DEPLOYMENT_IN_PROGRESS,
state: StackActionState.IN_PROGRESS,
changes: validationResult.changes,
});

const deploymentResult = await waitForDeployment(this.cfnService, stackName, changeSetType);

this.workflows.set(workflowId, {
...existingWorkflow,
existingWorkflow = processWorkflowUpdates(this.workflows, existingWorkflow, {
phase: deploymentResult.phase,
state: deploymentResult.state,
changes: validationResult.changes,
});
} catch (error) {
this.log.error({ error, workflowId }, 'Deployment workflow failed');
this.workflows.set(workflowId, {
...existingWorkflow,
phase: validationResult ? StackActionPhase.DEPLOYMENT_FAILED : StackActionPhase.VALIDATION_FAILED,
this.log.error({ error, workflowId }, 'Deployment workflow threw exception during deployment phase');
processWorkflowUpdates(this.workflows, existingWorkflow, {
phase: StackActionPhase.DEPLOYMENT_FAILED,
state: StackActionState.FAILED,
changes: validationResult?.changes,
failureReason: extractErrorMessage(error),
});
} finally {
await this.processDeploymentEvents(existingWorkflow, stackName); // Even if the deployment fails, some deployment events may have occurred
}
}

private async processDeploymentEvents(
existingWorkflow: StackActionWorkflowState,
stackName: string,
): Promise<void> {
try {
const stackEventsResponse = await this.cfnService.describeStackEvents(
{ StackName: stackName },
existingWorkflow.id,
);

const deploymentEvents: DeploymentEvent[] =
stackEventsResponse.StackEvents?.map((event) => ({
LogicalResourceId: event.LogicalResourceId,
ResourceType: event.ResourceType,
Timestamp: event.Timestamp ? DateTime.fromJSDate(event.Timestamp) : undefined,
ResourceStatus: event.ResourceStatus,
ResourceStatusReason: event.ResourceStatusReason,
DetailedStatus: event.DetailedStatus,
})) ?? [];

processWorkflowUpdates(this.workflows, existingWorkflow, {
deploymentEvents: deploymentEvents,
});
} catch (error) {
this.log.error({ error, stackName }, 'Failed to process deployment events');
}
}

Expand Down
26 changes: 20 additions & 6 deletions src/stacks/actions/StackActionOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export async function waitForValidation(
phase: StackActionPhase.VALIDATION_COMPLETE,
state: StackActionState.SUCCESSFUL,
changes: mapChangesToStackChanges(response.Changes),
reason: result.reason ? String(result.reason) : undefined,
failureReason: result.reason ? String(result.reason) : undefined,
};
} else {
logger.warn(
Expand All @@ -73,15 +73,15 @@ export async function waitForValidation(
return {
phase: StackActionPhase.VALIDATION_FAILED,
state: StackActionState.FAILED,
reason: result.reason ? String(result.reason) : undefined, // TODO: Return reason as part of LSP Response
failureReason: result.reason ? String(result.reason) : undefined,
};
}
} catch (error) {
logger.error({ error: extractErrorMessage(error) }, 'Validation failed with error');
return {
phase: StackActionPhase.VALIDATION_FAILED,
state: StackActionState.FAILED,
reason: extractErrorMessage(error),
failureReason: extractErrorMessage(error),
};
}
}
Expand All @@ -105,7 +105,7 @@ export async function waitForDeployment(
return {
phase: StackActionPhase.DEPLOYMENT_COMPLETE,
state: StackActionState.SUCCESSFUL,
reason: result.reason ? String(result.reason) : undefined,
failureReason: result.reason ? String(result.reason) : undefined,
};
} else {
logger.warn(
Expand All @@ -115,15 +115,15 @@ export async function waitForDeployment(
return {
phase: StackActionPhase.DEPLOYMENT_FAILED,
state: StackActionState.FAILED,
reason: result.reason ? String(result.reason) : undefined, // TODO: Return reason as part of LSP Response
failureReason: result.reason ? String(result.reason) : undefined,
};
}
} catch (error) {
logger.error({ error: extractErrorMessage(error) }, 'Deployment failed with error');
return {
phase: StackActionPhase.DEPLOYMENT_FAILED,
state: StackActionState.FAILED,
reason: extractErrorMessage(error),
failureReason: extractErrorMessage(error),
};
}
}
Expand Down Expand Up @@ -214,3 +214,17 @@ export function mapChangesToStackChanges(changes?: Change[]): StackChange[] | un
: undefined,
}));
}

export function processWorkflowUpdates(
workflows: Map<string, StackActionWorkflowState>,
existingWorkflow: StackActionWorkflowState,
workflowUpdates: Partial<StackActionWorkflowState>,
): StackActionWorkflowState {
existingWorkflow = {
...existingWorkflow,
...workflowUpdates,
};
workflows.set(existingWorkflow.id, existingWorkflow);

return existingWorkflow;
}
Loading
Loading