@chimoney.io/iaas-k8s-deployment
Version:
Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments
407 lines (356 loc) • 11 kB
text/typescript
import type {
Logger,
DeploymentProgress,
DeploymentResult,
} from "../types/index.js";
// =============================================================================
// Metrics and Monitoring
// =============================================================================
export interface DeploymentMetrics {
deploymentId: string;
startTime: Date;
endTime?: Date;
duration?: number;
action: string;
stackName: string;
companyName: string;
success: boolean;
errorType?: string;
resourcesCreated?: number;
resourcesUpdated?: number;
resourcesDeleted?: number;
rollbackPerformed?: boolean;
progressEvents: DeploymentProgress[];
}
export interface HealthCheck {
name: string;
status: "healthy" | "unhealthy" | "unknown";
message: string;
timestamp: Date;
details?: Record<string, any>;
}
export class DeploymentMonitor {
private metrics: DeploymentMetrics;
private logger: Logger;
private healthChecks: HealthCheck[] = [];
constructor(
deploymentId: string,
action: string,
stackName: string,
companyName: string,
logger: Logger
) {
this.metrics = {
deploymentId,
startTime: new Date(),
action,
stackName,
companyName,
success: false,
progressEvents: [],
};
this.logger = logger;
}
recordProgress(progress: DeploymentProgress): void {
this.metrics.progressEvents.push(progress);
this.logger.info(`Progress: ${progress.status} - ${progress.message}`);
}
recordCompletion(result: DeploymentResult): void {
this.metrics.endTime = new Date();
this.metrics.duration =
this.metrics.endTime.getTime() - this.metrics.startTime.getTime();
this.metrics.success = result.success;
this.metrics.rollbackPerformed = result.rollbackPerformed;
if (!result.success && result.error) {
this.metrics.errorType = this.categorizeError(result.error);
}
if (result.summary) {
this.extractResourceChanges(result.summary);
}
this.logger.info("Deployment metrics recorded", this.getMetricsSummary());
}
private categorizeError(error: string): string {
if (error.includes("timeout") || error.includes("TIMEOUT")) {
return "TIMEOUT";
}
if (error.includes("permission") || error.includes("unauthorized")) {
return "PERMISSION";
}
if (error.includes("validation") || error.includes("invalid")) {
return "VALIDATION";
}
if (error.includes("network") || error.includes("connection")) {
return "NETWORK";
}
if (error.includes("resource") && error.includes("conflict")) {
return "RESOURCE_CONFLICT";
}
return "UNKNOWN";
}
private extractResourceChanges(summary: any): void {
if (summary.resourceChanges) {
this.metrics.resourcesCreated = summary.resourceChanges.create || 0;
this.metrics.resourcesUpdated = summary.resourceChanges.update || 0;
this.metrics.resourcesDeleted = summary.resourceChanges.delete || 0;
}
}
async performHealthChecks(): Promise<HealthCheck[]> {
this.healthChecks = [];
// Check Pulumi CLI availability
await this.checkPulumiCli();
// Check cloud provider credentials
await this.checkCloudCredentials();
// Check Helm chart accessibility
await this.checkHelmChart();
// Check network connectivity
await this.checkNetworkConnectivity();
return this.healthChecks;
}
private async checkPulumiCli(): Promise<void> {
try {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const { stdout } = await execAsync("pulumi version");
this.addHealthCheck(
"pulumi-cli",
"healthy",
`Pulumi CLI available: ${stdout.trim()}`
);
} catch (error) {
this.addHealthCheck(
"pulumi-cli",
"unhealthy",
`Pulumi CLI not available: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
private async checkCloudCredentials(): Promise<void> {
const cloudProvider = process.env.CLOUD_PROVIDER || "gcp";
try {
if (cloudProvider === "aws") {
await this.checkAwsCredentials();
} else if (cloudProvider === "gcp") {
await this.checkGcpCredentials();
}
} catch (error) {
this.addHealthCheck(
"cloud-credentials",
"unhealthy",
`Cloud credentials check failed: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
private async checkAwsCredentials(): Promise<void> {
try {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const { stdout } = await execAsync("aws sts get-caller-identity");
const identity = JSON.parse(stdout);
this.addHealthCheck(
"aws-credentials",
"healthy",
`AWS credentials valid for user: ${identity.UserId}`,
{ account: identity.Account, arn: identity.Arn }
);
} catch (error) {
this.addHealthCheck(
"aws-credentials",
"unhealthy",
"AWS credentials not configured or invalid"
);
}
}
private async checkGcpCredentials(): Promise<void> {
try {
const { exec } = await import("child_process");
const { promisify } = await import("util");
const execAsync = promisify(exec);
const { stdout } = await execAsync(
'gcloud auth list --format="value(account)" --filter="status:ACTIVE"'
);
const activeAccount = stdout.trim();
if (activeAccount) {
this.addHealthCheck(
"gcp-credentials",
"healthy",
`GCP credentials valid for account: ${activeAccount}`
);
} else {
this.addHealthCheck(
"gcp-credentials",
"unhealthy",
"No active GCP account found"
);
}
} catch (error) {
this.addHealthCheck(
"gcp-credentials",
"unhealthy",
"GCP credentials not configured or invalid"
);
}
}
private async checkHelmChart(): Promise<void> {
const helmChartPath = process.env.HELM_CHART_PATH || "../helm-chart";
try {
const fs = await import("fs");
const path = await import("path");
const chartYamlPath = path.join(helmChartPath, "Chart.yaml");
if (fs.existsSync(chartYamlPath)) {
const chartContent = fs.readFileSync(chartYamlPath, "utf8");
this.addHealthCheck(
"helm-chart",
"healthy",
`Helm chart accessible at: ${helmChartPath}`,
{ path: chartYamlPath }
);
} else {
this.addHealthCheck(
"helm-chart",
"unhealthy",
`Helm chart not found at: ${chartYamlPath}`
);
}
} catch (error) {
this.addHealthCheck(
"helm-chart",
"unhealthy",
`Error checking Helm chart: ${
error instanceof Error ? error.message : String(error)
}`
);
}
}
private async checkNetworkConnectivity(): Promise<void> {
const cloudProvider = process.env.CLOUD_PROVIDER || "gcp";
try {
const testUrls =
cloudProvider === "aws"
? ["https://aws.amazon.com", "https://s3.amazonaws.com"]
: ["https://cloud.google.com", "https://storage.googleapis.com"];
// Simple connectivity check (in a real implementation, you might use actual SDK calls)
this.addHealthCheck(
"network-connectivity",
"healthy",
`Network connectivity assumed healthy for ${cloudProvider}`
);
} catch (error) {
this.addHealthCheck(
"network-connectivity",
"unhealthy",
"Network connectivity issues detected"
);
}
}
private addHealthCheck(
name: string,
status: HealthCheck["status"],
message: string,
details?: Record<string, any>
): void {
this.healthChecks.push({
name,
status,
message,
timestamp: new Date(),
details,
});
}
getMetrics(): DeploymentMetrics {
return { ...this.metrics };
}
getMetricsSummary(): object {
return {
deploymentId: this.metrics.deploymentId,
duration: this.metrics.duration,
success: this.metrics.success,
resourceChanges: {
created: this.metrics.resourcesCreated || 0,
updated: this.metrics.resourcesUpdated || 0,
deleted: this.metrics.resourcesDeleted || 0,
},
progressEventCount: this.metrics.progressEvents.length,
rollbackPerformed: this.metrics.rollbackPerformed,
};
}
getHealthStatus(): {
overall: "healthy" | "unhealthy";
checks: HealthCheck[];
} {
const hasUnhealthy = this.healthChecks.some(
(check) => check.status === "unhealthy"
);
return {
overall: hasUnhealthy ? "unhealthy" : "healthy",
checks: this.healthChecks,
};
}
exportMetrics(): string {
return JSON.stringify(
{
...this.getMetrics(),
healthChecks: this.healthChecks,
},
null,
2
);
}
}
// =============================================================================
// Deployment Reporter
// =============================================================================
export class DeploymentReporter {
static generateReport(
metrics: DeploymentMetrics,
healthChecks: HealthCheck[]
): string {
const duration = metrics.duration
? `${(metrics.duration / 1000).toFixed(2)}s`
: "N/A";
const successIcon = metrics.success ? "✅" : "❌";
let report = `
# Deployment Report ${successIcon}
## Summary
- **Deployment ID**: ${metrics.deploymentId}
- **Action**: ${metrics.action}
- **Stack**: ${metrics.stackName}
- **Company**: ${metrics.companyName}
- **Status**: ${metrics.success ? "SUCCESS" : "FAILED"}
- **Duration**: ${duration}
- **Start Time**: ${metrics.startTime.toISOString()}
- **End Time**: ${metrics.endTime?.toISOString() || "N/A"}
## Resource Changes
- **Created**: ${metrics.resourcesCreated || 0}
- **Updated**: ${metrics.resourcesUpdated || 0}
- **Deleted**: ${metrics.resourcesDeleted || 0}
## Progress Timeline
`;
metrics.progressEvents.forEach((event, index) => {
report += `${index + 1}. **${event.status}** - ${
event.message
} (${event.timestamp.toISOString()})\n`;
});
if (metrics.rollbackPerformed) {
report += `\n⚠️ **Rollback Performed**: Automatic rollback was executed due to deployment failure.\n`;
}
if (healthChecks.length > 0) {
report += `\n## Health Checks\n`;
healthChecks.forEach((check) => {
const icon =
check.status === "healthy"
? "✅"
: check.status === "unhealthy"
? "❌"
: "⚠️";
report += `- ${icon} **${check.name}**: ${check.message}\n`;
});
}
return report;
}
}