@chimoney.io/iaas-k8s-deployment
Version:
Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments
211 lines (183 loc) • 6.77 kB
text/typescript
import { automation } from "@pulumi/pulumi";
import type {
Logger,
DeploymentAction,
DeploymentResult,
} from "../../types/index.js";
import { DeploymentError } from "../errors/deployment-errors.js";
import { storeDeploymentSnapshot } from "../rollback/rollback-manager.js";
// =============================================================================
// Error Handling Wrapper
// =============================================================================
export async function withErrorHandling<T>(
operation: () => Promise<T>,
logger: Logger,
context: string
): Promise<T> {
try {
logger.debug(`Starting operation: ${context}`);
const result = await operation();
logger.debug(`Completed operation: ${context}`);
return result;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error(`Error in ${context}:`, errorMessage);
if (error instanceof Error && error.stack) {
logger.debug("Stack trace:", error.stack);
}
// Re-throw as DeploymentError if not already
if (!(error instanceof DeploymentError)) {
throw new DeploymentError(
`${context} failed: ${errorMessage}`,
"OPERATION_FAILED",
error
);
}
throw error;
}
}
// =============================================================================
// Deployment Action Executor
// =============================================================================
export async function executeDeploymentAction(
action: DeploymentAction,
stack: automation.Stack,
logger: Logger,
stackName: string
): Promise<Partial<DeploymentResult>> {
switch (action) {
case "up":
logger.info(`Running pulumi up for stack: ${stackName}...`);
let lastOutputTime = Date.now();
const progressInterval = setInterval(() => {
const timeSinceLastOutput = Date.now() - lastOutputTime;
if (timeSinceLastOutput > 30000) {
// 30 seconds without output
logger.info(
`[PROGRESS] Deployment still running... (${Math.floor(
timeSinceLastOutput / 1000
)}s since last output)`
);
}
}, 30000);
try {
const upRes: automation.UpResult = await stack.up({
onOutput: (output) => {
lastOutputTime = Date.now();
// Show Pulumi output at info level to ensure visibility
logger.info(`[PULUMI] ${output.trim()}`);
},
refresh: true, // Ensure we refresh state before updating
});
clearInterval(progressInterval);
logger.info("--- Update Summary ---");
if (upRes.summary) {
logger.info(`Status: ${upRes.summary.result}`);
logger.info(`Version: ${upRes.summary.version}`);
logger.debug(
"Resource changes:",
JSON.stringify(upRes.summary.resourceChanges, null, 2)
);
}
logger.info("--- Outputs ---");
logger.debug("Stack outputs:", JSON.stringify(upRes.outputs, null, 2));
// Store configuration snapshot for successful deployments
if (upRes.summary?.result === "succeeded") {
try {
await storeDeploymentSnapshot(stack, logger, upRes);
logger.debug(
"Configuration snapshot stored for successful deployment"
);
} catch (snapshotError) {
logger.warn(
"Failed to store configuration snapshot:",
snapshotError
);
// Don't fail the deployment for snapshot errors
}
}
let kubeconfig: string | undefined;
if (upRes.outputs.kubeconfig && upRes.outputs.kubeconfig.value) {
kubeconfig =
typeof upRes.outputs.kubeconfig.value === "string"
? upRes.outputs.kubeconfig.value
: JSON.stringify(upRes.outputs.kubeconfig.value);
logger.info("Kubeconfig is available in deployment outputs");
logger.debug("To configure kubectl:");
logger.debug(
`1. Save kubeconfig to file: kubeconfig-${stackName}.yaml`
);
logger.debug(`2. Set KUBECONFIG environment variable`);
logger.debug(`3. Test connection with: kubectl get nodes`);
}
return {
outputs: upRes.outputs,
summary: upRes.summary,
kubeconfig,
};
} catch (error) {
clearInterval(progressInterval);
throw error;
}
case "preview":
logger.info(`Running pulumi preview for stack: ${stackName}...`);
await stack.preview({
onOutput: (output) => logger.info(`[PULUMI] ${output.trim()}`),
onError: (error) => logger.error(error),
});
logger.info("Preview finished successfully");
return {};
case "destroy":
logger.info(`Running pulumi destroy for stack: ${stackName}...`);
const destroyRes = await stack.destroy({
onOutput: (output) => {
// Show Pulumi output at info level to ensure visibility during destroy
logger.info(`[PULUMI] ${output.trim()}`);
},
});
logger.info("--- Destroy Summary ---");
if (destroyRes.summary) {
logger.info(`Status: ${destroyRes.summary.result}`);
logger.debug(
"Resource changes:",
JSON.stringify(destroyRes.summary.resourceChanges, null, 2)
);
}
return {
summary: destroyRes.summary,
};
case "outputs":
logger.info(`Fetching outputs for stack: ${stackName}...`);
const outputs = await stack.outputs();
logger.info("--- Stack Outputs ---");
logger.info("Outputs:", JSON.stringify(outputs, null, 2));
return {
outputs: outputs,
};
case "refresh":
logger.info(`Running pulumi refresh for stack: ${stackName}...`);
const refreshRes = await stack.refresh({
onOutput: (output) => logger.info(`[PULUMI] ${output.trim()}`),
});
logger.info("--- Refresh Summary ---");
if (refreshRes.summary) {
logger.info(`Status: ${refreshRes.summary.result}`);
}
return {
summary: refreshRes.summary,
};
case "rollback":
logger.info(`Running rollback for stack: ${stackName}...`);
// Rollback is handled in the main function, this is just for completeness
throw new DeploymentError(
"Rollback should be handled in main deployment function",
"INVALID_ACTION"
);
default:
throw new DeploymentError(
`Unknown action: ${action}. Available actions: up, preview, destroy, outputs, refresh, rollback`,
"INVALID_ACTION",
{ action }
);
}
}