@chimoney.io/iaas-k8s-deployment
Version:
Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments
391 lines • 20.9 kB
JavaScript
import { automation } from "@pulumi/pulumi";
import { validateDeploymentOptionsWithZod, validateSecretsStructure, validateAndProcessSecrets, validateNamespaceConfiguration, } from "../../utils/validation.js";
import { DeploymentMonitor } from "../../utils/monitoring.js";
import { PulumiConfigManager } from "../../utils/config-manager.js";
import { TierCalculator } from "../../utils/tier-calculator.js";
// Import the separated modules
import { DeploymentError, ConfigValidationError, } from "../errors/deployment-errors.js";
import { ConsoleLogger } from "../logging/console-logger.js";
import { validateDeploymentConfig } from "../validation/config-validator.js";
import { createProgressCallback } from "../progress/progress-tracker.js";
import { performRollback } from "../rollback/rollback-manager.js";
import { generateDynamicHelmValues, mergeHelmValues, } from "../helm/helm-values-generator.js";
import { withErrorHandling, executeDeploymentAction, } from "./deployment-executor.js";
// =============================================================================
// Main Deployment Handler
// =============================================================================
export async function handleDeployment(options) {
const startTime = Date.now();
const { action, stackName, // Keep original stackName for constructing the FQSN
secretsJson, companyName, // This will be used as the organization
workDir, helmChartPath, logLevel = "info", onProgress, validateConfig = true, enableRollback = true, timeout = 1800, // 30 minutes default
// New namespace-based deployment parameters
namespace, deploymentType, } = options;
const projectName = "iaas-k8s";
const organizationName = process.env.PULUMI_ORG_NAME || "organization"; // Using "organization", needed for Pulumi LocalWorkspace
// Construct the fully qualified stack name based on deployment type
let fullyQualifiedStackName;
if (deploymentType === "shared") {
// For shared deployments, use the namespace in the stack name
const effectiveNamespace = namespace || `${companyName}-${stackName}`.toLowerCase();
fullyQualifiedStackName = `${organizationName}/${projectName}/shared-${effectiveNamespace}`;
}
else {
// For dedicated deployments, use the original company-based naming
fullyQualifiedStackName = `${organizationName}/${projectName}/${companyName}-${stackName}`;
}
// Initialize logger and progress callback
const logger = new ConsoleLogger(logLevel);
const reportProgress = createProgressCallback(logger, onProgress);
// Initialize deployment monitor
const deploymentId = `${companyName}-${fullyQualifiedStackName}-${Date.now()}`;
const monitor = new DeploymentMonitor(deploymentId, action, fullyQualifiedStackName, // Use fully qualified name
companyName, logger);
let rollbackPerformed = false;
let stack;
let result;
try {
reportProgress("initializing", "Starting deployment initialization");
monitor.recordProgress({
status: "initializing",
message: "Starting deployment initialization",
timestamp: new Date(),
});
// =============================================================================
// Enhanced Configuration Validation
// =============================================================================
if (validateConfig) {
logger.debug("Validating deployment configuration with enhanced validation");
// Use Zod validation first
const zodValidationErrors = validateDeploymentOptionsWithZod(options);
if (zodValidationErrors.length > 0) {
const errorMessage = `Enhanced validation failed: ${zodValidationErrors
.map((e) => `${e.field}: ${e.message}`)
.join(", ")}`;
throw new ConfigValidationError(errorMessage, zodValidationErrors);
}
// Additional structure validation
const secretErrors = validateSecretsStructure(secretsJson);
if (secretErrors.length > 0) {
const errorMessage = `Configuration structure validation failed: ${secretErrors
.map((e) => `${e.field}: ${e.message}`)
.join(", ")}`;
throw new ConfigValidationError(errorMessage, secretErrors);
}
logger.debug("Enhanced configuration validation passed");
}
// =============================================================================
// Namespace and Deployment Type Validation
// =============================================================================
if (deploymentType === "shared" && !namespace) {
// Generate namespace automatically for shared deployments if not provided
const generatedNamespace = `${companyName}-${stackName}`.toLowerCase();
logger.info(`Generated namespace for shared deployment: ${generatedNamespace}`);
options.namespace = generatedNamespace;
}
if (namespace || deploymentType === "shared") {
const effectiveNamespace = namespace || options.namespace;
const namespaceValidationErrors = validateNamespaceConfiguration(effectiveNamespace, deploymentType);
if (namespaceValidationErrors.length > 0) {
const errorMessage = `Namespace validation failed: ${namespaceValidationErrors
.map((e) => `${e.field}: ${e.message}`)
.join(", ")}`;
throw new ConfigValidationError(errorMessage, namespaceValidationErrors);
}
logger.info(`Using namespace: ${effectiveNamespace} for ${deploymentType} deployment`);
}
// =============================================================================
// Tier-Based Resource Allocation Validation
// =============================================================================
let tierAllocationResult;
if (options.planTier && deploymentType === "shared") {
logger.info(`Processing tier-based deployment for ${options.planTier} tier`);
const tierCalculator = new TierCalculator(logger);
const effectiveNamespace = namespace || options.namespace;
// Calculate tier allocation for resource planning
tierAllocationResult = tierCalculator.calculateTierAllocation(options.planTier, companyName, effectiveNamespace, {
kubecostEnabled: false, // Kubecost integration removed
billingAccountId: options.billingAccountId,
});
if (!tierAllocationResult.success) {
const errorMessage = `Tier allocation failed: ${tierAllocationResult.errors.join(", ")}`;
logger.error(errorMessage);
throw new ConfigValidationError(errorMessage, []);
}
if (tierAllocationResult.warnings.length > 0) {
tierAllocationResult.warnings.forEach((warning) => {
logger.warn(`Tier warning: ${warning}`);
});
}
logger.info(`Tier allocation calculated successfully for ${options.planTier} tier`);
logger.debug(`Estimated monthly cost: $${tierAllocationResult.estimatedCosts.monthly}`);
}
else if (options.planTier && deploymentType === "dedicated") {
logger.warn("Plan tier specified for dedicated deployment - tier-based resource allocation is only supported for shared deployments");
}
// =============================================================================
// Health Checks
// =============================================================================
reportProgress("initializing", "Performing pre-deployment health checks");
const healthChecks = await monitor.performHealthChecks();
const healthStatus = monitor.getHealthStatus();
if (healthStatus.overall === "unhealthy") {
const unhealthyChecks = healthChecks.filter((c) => c.status === "unhealthy");
const errorMessage = `Pre-deployment health checks failed: ${unhealthyChecks
.map((c) => `${c.name}: ${c.message}`)
.join(", ")}`;
logger.warn("Health check failures detected:", errorMessage);
// Continue with deployment but log warnings
}
else {
logger.info("All health checks passed");
}
// Additional legacy validation for backward compatibility
if (!validateConfig) {
logger.debug("Performing basic configuration validation");
const config = {
stackName: fullyQualifiedStackName, // Use fully qualified name
secretsJson,
companyName,
helmChartPath,
};
const validationErrors = validateDeploymentConfig(config);
if (validationErrors.length > 0) {
const errorMessage = validationErrors
.map((e) => `${e.field}: ${e.message}`)
.join(", ");
throw new ConfigValidationError(`Configuration validation failed: ${errorMessage}`, validationErrors);
}
}
// =============================================================================
// Stack Initialization
// =============================================================================
const resolvedWorkDir = workDir || process.cwd();
const projectSettings = {
stackName: fullyQualifiedStackName,
workDir: resolvedWorkDir,
};
stack = await withErrorHandling(() => automation.LocalWorkspace.createOrSelectStack(projectSettings), logger, "stack initialization");
logger.info(`Successfully initialized stack: ${stack.name}`);
logger.info(`Working directory for Pulumi program: ${resolvedWorkDir}`);
// =============================================================================
// Automatic Pulumi Config Setup (if enabled)
// =============================================================================
if (options.autoSetupConfig &&
options.cloudProvider &&
options.cloudConfig) {
logger.info("Auto-setup of Pulumi config enabled. Setting up stack config...");
const configManager = new PulumiConfigManager(logger);
await configManager.setupPulumiConfig(resolvedWorkDir, stack, {
stackName,
cloudProvider: options.cloudProvider,
companyName,
cloudConfig: options.cloudConfig,
secretsJson,
helmChartPath,
});
logger.info("Pulumi config auto-setup complete.");
}
// =============================================================================
// Configuration Setup
// =============================================================================
reportProgress("configuring", "Setting up stack configuration");
// Set configuration with validation and encoding
await withErrorHandling(async () => {
// Validate and process secrets JSON for Kubernetes (using stringData - no encoding needed)
logger.info("Validating and processing secrets for Kubernetes deployment...");
const secretsValidation = validateAndProcessSecrets(secretsJson);
if (!secretsValidation.isValid) {
const errorMessages = secretsValidation.errors
.map((e) => `${e.field}: ${e.message}`)
.join(", ");
throw new ConfigValidationError(`Secrets validation failed during configuration setup: ${errorMessages}`, secretsValidation.errors);
}
logger.info("Secrets validation passed");
// Log processing report if available
if (secretsValidation.processingReport) {
const processingStats = Object.entries(secretsValidation.processingReport).reduce((acc, [key, status]) => {
acc[status] = (acc[status] || 0) + 1;
return acc;
}, {});
logger.info(`Secret processing complete: ${JSON.stringify(processingStats)}`);
}
// Set the processed secrets JSON (plain text for stringData)
await stack.setConfig("helmSecretsJson", {
value: secretsValidation.processedSecretsJson,
secret: true,
});
logger.info("Set helmSecretsJson configuration with processed secrets for stringData (as secret)");
// Generate dynamic Helm values
logger.info("Generating dynamic Helm chart values with Kubernetes secrets enabled");
const dynamicHelmValues = generateDynamicHelmValues(options, logger);
// Merge with tier-based values if applicable
let finalHelmValues = dynamicHelmValues;
if (tierAllocationResult && tierAllocationResult.success) {
logger.info(`Merging tier-based values for ${options.planTier} tier`);
finalHelmValues = mergeHelmValues(dynamicHelmValues, tierAllocationResult.helmValues, logger);
}
const finalValuesJson = JSON.stringify(finalHelmValues);
await stack.setConfig("helmValuesJson", { value: finalValuesJson });
logger.info("Set helmValuesJson configuration with dynamically generated values");
// Set company name
await stack.setConfig("companyName", { value: companyName });
logger.info("Set companyName configuration");
// Set cloud provider config
if (options.cloudProvider) {
await stack.setConfig("cloudProvider", {
value: options.cloudProvider,
});
logger.info(`Set cloudProvider configuration: ${options.cloudProvider}`);
}
// Set Helm chart path if provided
if (helmChartPath) {
await stack.setConfig("helmChartPath", { value: helmChartPath });
logger.info("Set helmChartPath configuration");
}
// Set namespace and deployment type configuration
const effectiveNamespace = namespace || options.namespace;
if (effectiveNamespace) {
await stack.setConfig("namespace", { value: effectiveNamespace });
logger.info(`Set namespace configuration: ${effectiveNamespace}`);
}
await stack.setConfig("deploymentType", { value: deploymentType });
logger.info(`Set deploymentType configuration: ${deploymentType}`);
// Set tier-based configuration if applicable
if (options.planTier) {
await stack.setConfig("planTier", { value: options.planTier });
logger.info(`Set planTier configuration: ${options.planTier}`);
}
}, logger, "configuration setup");
// =============================================================================
// Execute Deployment Action
// =============================================================================
reportProgress("deploying", `Executing ${action} operation`);
if (action === "rollback") {
try {
reportProgress("rolling-back", "Starting manual rollback operation");
await performRollback(stack, logger, reportProgress);
// Get the final stack outputs after rollback
const outputs = await stack.outputs();
result = {
success: true,
outputs,
rollbackPerformed: true,
duration: Date.now() - startTime,
};
reportProgress("completed", "Rollback operation completed successfully");
logger.info("Rollback operation completed successfully");
return result;
}
catch (rollbackError) {
const errorMessage = rollbackError instanceof Error
? rollbackError.message
: String(rollbackError);
logger.error("Rollback operation failed:", errorMessage);
return {
success: false,
error: errorMessage,
rollbackPerformed: false,
duration: Date.now() - startTime,
};
}
}
// Set up timeout for the operation
const timeoutMs = timeout * 1000;
const operationPromise = executeDeploymentAction(action, stack, logger, stackName);
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => {
reject(new DeploymentError(`Operation timed out after ${timeout} seconds`, "TIMEOUT", { timeout, action }));
}, timeoutMs);
});
const operationResult = await Promise.race([
operationPromise,
timeoutPromise,
]);
// Initialize the result with basic information
result = {
success: true,
...operationResult,
duration: Date.now() - startTime,
};
// Add tier-specific information to the result if available
if (tierAllocationResult && tierAllocationResult.success) {
// Cast to TierDeploymentResult type to add tierInfo
result = {
success: true,
...operationResult,
tierInfo: {
planTier: options.planTier,
resourceAllocation: tierAllocationResult.resourceAllocation,
costEstimate: {
monthly: tierAllocationResult.estimatedCosts.monthly,
currency: "USD",
},
costMonitoring: {
enabled: false, // Kubecost integration removed
dashboardUrl: undefined,
budgetAllocated: undefined,
budgetThresholds: [75, 90, 100], // Standard thresholds
alertsEnabled: false,
},
},
duration: Date.now() - startTime,
};
}
else {
// Standard result format
result = {
success: true,
...operationResult,
duration: Date.now() - startTime,
};
}
reportProgress("completed", `${action} operation completed successfully`);
logger.info("Pulumi operation completed successfully");
return result;
}
catch (error) {
// =============================================================================
// Error Handling and Rollback
// =============================================================================
logger.error("Pulumi operation failed:", error instanceof Error ? error.message : String(error));
let rollbackError;
// Attempt rollback if enabled and this was a deployment that failed
if (enableRollback && action === "up" && stack && !rollbackPerformed) {
try {
reportProgress("rolling-back", "Attempting automatic rollback");
await performRollback(stack, logger, reportProgress);
rollbackPerformed = true;
logger.info("Automatic rollback completed successfully");
}
catch (rbError) {
rollbackError =
rbError instanceof Error ? rbError : new Error(String(rbError));
logger.error("Rollback failed:", rollbackError.message);
}
}
// Format error message
let errorMessage = "Unknown error occurred";
if (error instanceof Error) {
errorMessage = error.message;
// Log detailed error information
if (error.stack) {
logger.debug("Stack trace:", error.stack);
}
}
else {
errorMessage = String(error);
}
// Include rollback error in the message if it occurred
if (rollbackError) {
errorMessage += ` (Rollback also failed: ${rollbackError.message})`;
}
return {
success: false,
error: errorMessage,
rollbackPerformed,
duration: Date.now() - startTime,
};
}
}
//# sourceMappingURL=deployment-handler.js.map