UNPKG

@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
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 } ); } }