@chimoney.io/iaas-k8s-deployment
Version:
Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments
1,488 lines (1,341 loc) • 43.5 kB
text/typescript
/**
* Kubecost API Client
*
* This module provides functions to interact with Kubecost for cost tracking,
* usage metrics collection, and billing integration for tier-based deployments.
*/
import { Logger } from "../types/index.js";
import { PlanTier, KubecostConfig } from "../types/plans.js";
import { GoogleAuth } from "google-auth-library";
export interface KubecostAllocation {
name: string;
properties: {
cluster: string;
node: string;
container: string;
controller: string;
namespace: string;
pod: string;
providerID: string;
labels?: Record<string, string>;
};
start: string;
end: string;
minutes: number;
cpuCores: number;
cpuCoreRequestAverage: number;
cpuCoreUsageAverage: number;
cpuCoreHours: number;
cpuCost: number;
cpuCostAdjustment: number;
cpuEfficiency: number;
gpuCount: number;
gpuHours: number;
gpuCost: number;
gpuCostAdjustment: number;
networkReceiveBytes: number;
networkTransferBytes: number;
networkCost: number;
networkCostAdjustment: number;
loadBalancerCost: number;
loadBalancerCostAdjustment: number;
pvBytes: number;
pvByteHours: number;
pvCost: number;
pvs?: Record<string, any>;
ramBytes: number;
ramByteRequestAverage: number;
ramByteUsageAverage: number;
ramByteHours: number;
ramCost: number;
ramCostAdjustment: number;
ramEfficiency: number;
externalCost: number;
sharedCost: number;
totalCost: number;
totalEfficiency: number;
}
export interface KubecostAsset {
type: string;
name: string;
properties: {
category: string;
cluster: string;
node?: string;
providerID: string;
labels?: Record<string, string>;
};
labels?: Record<string, string>;
start: string;
end: string;
minutes: number;
byteHours?: number;
bytes?: number;
breakdown?: Record<string, number>;
adjustment: number;
totalCost: number;
}
export interface TierCostSummary {
tier: PlanTier;
namespace: string;
company: string;
period: {
start: string;
end: string;
};
costs: {
cpu: number;
memory: number;
storage: number;
network: number;
loadBalancer: number;
total: number;
currency: string;
};
efficiency: {
cpu: number;
memory: number;
overall: number;
};
usage: {
cpuCoreHours: number;
ramGBHours: number;
storageGBHours: number;
};
budgetStatus: {
allocated: number;
used: number;
remaining: number;
utilizationPercent: number;
};
}
export interface CostAlert {
id: string;
type: "budget" | "anomaly" | "efficiency";
severity: "info" | "warning" | "critical";
title: string;
message: string;
tier: PlanTier;
namespace: string;
company: string;
threshold: number;
current: number;
timestamp: string;
metadata?: Record<string, any>;
}
export interface KubecostQueryOptions {
window: string; // e.g., "7d", "30d", "1h"
step?: string; // e.g., "1d", "1h"
aggregate?: string; // e.g., "namespace", "label:tier"
accumulate?: boolean; // Return cumulative values
includeIdle?: boolean; // Include idle costs
format?: "json" | "csv"; // Response format
filter?: string; // Filter expression
}
/**
* GCP Billing Account configuration
*/
export interface GCPBillingConfig {
billingAccountId: string;
projectId: string;
credentialsPath?: string;
serviceAccountKey?: string;
}
/**
* GCP Billing API response structure
*/
export interface GCPBillingData {
name: string;
displayName: string;
open: boolean;
masterBillingAccount?: string;
}
export interface GCPCostData {
name: string;
displayName: string;
skuId: string;
skuDisplayName: string;
usage: {
unit: string;
amount: number;
amountInPricingUnits: number;
};
cost: {
currencyCode: string;
units: string;
nanos: number;
};
creditAdjustments: Array<{
name: string;
displayName: string;
type: string;
amount: {
currencyCode: string;
units: string;
nanos: number;
};
}>;
}
export class KubecostClient {
private baseUrl: string;
private apiKey?: string;
private logger: Logger;
private config: KubecostConfig;
private gcpBillingConfig?: GCPBillingConfig;
private googleAuth?: GoogleAuth;
constructor(
baseUrl: string,
config: KubecostConfig,
logger: Logger,
apiKey?: string,
gcpBillingConfig?: GCPBillingConfig
) {
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
this.apiKey = apiKey;
this.logger = logger;
this.config = config;
this.gcpBillingConfig = gcpBillingConfig;
if (gcpBillingConfig) {
this.initializeGCPAuth();
}
}
/**
* Initialize GCP authentication for billing API access
*/
private async initializeGCPAuth(): Promise<void> {
if (!this.gcpBillingConfig) {
throw new Error("GCP billing configuration not provided");
}
try {
this.googleAuth = new GoogleAuth({
keyFilename: this.gcpBillingConfig.credentialsPath,
credentials: this.gcpBillingConfig.serviceAccountKey
? JSON.parse(this.gcpBillingConfig.serviceAccountKey)
: undefined,
scopes: [
"https://www.googleapis.com/auth/cloud-platform",
"https://www.googleapis.com/auth/cloud-billing",
"https://www.googleapis.com/auth/cloud-billing.readonly",
],
});
// Test authentication
const client = await this.googleAuth.getClient();
await client.getAccessToken();
this.logger.info("GCP authentication initialized successfully");
} catch (error) {
this.logger.error(`Failed to initialize GCP authentication: ${error}`);
throw new Error(`GCP authentication failed: ${error}`);
}
}
/**
* Get cost allocation data for a specific namespace/tier
*/
async getAllocationData(
namespace: string,
options: KubecostQueryOptions = { window: "7d" }
): Promise<KubecostAllocation[]> {
try {
const params = new URLSearchParams();
params.set("window", options.window);
params.set("aggregate", options.aggregate || "namespace");
params.set("accumulate", String(options.accumulate || false));
params.set("includeIdle", String(options.includeIdle || false));
params.set("format", options.format || "json");
if (options.step) {
params.set("step", options.step);
}
if (namespace !== "*") {
params.set("filter", `namespace:"${namespace}"`);
}
if (options.filter) {
params.set("filter", options.filter);
}
const url = `${this.baseUrl}/model/allocation?${params.toString()}`;
const response = await this.makeRequest(url);
if (!response.data) {
throw new Error("No allocation data returned from Kubecost");
}
return Object.values(response.data[0] || {}) as KubecostAllocation[];
} catch (error) {
this.logger.error(
`Failed to get allocation data for namespace ${namespace}: ${error}`
);
throw error;
}
}
/**
* Get asset cost data (storage, load balancers, etc.)
*/
async getAssetData(
options: KubecostQueryOptions = { window: "7d" }
): Promise<KubecostAsset[]> {
try {
const params = new URLSearchParams();
params.set("window", options.window);
params.set("aggregate", "type");
params.set("accumulate", String(options.accumulate || false));
params.set("format", "json");
if (options.step) params.set("step", options.step);
if (options.includeIdle !== undefined)
params.set("includeIdle", String(options.includeIdle));
if (options.filter) params.set("filter", options.filter);
const url = `${this.baseUrl}/model/assets?${params.toString()}`;
const response = await this.makeRequest(url);
if (!response.data) {
throw new Error("No asset data returned from Kubecost");
}
return Object.values(response.data[0] || {}) as KubecostAsset[];
} catch (error) {
this.logger.error(`Failed to get asset data: ${error}`);
throw error;
}
}
/**
* Get comprehensive cost summary for a tier/namespace
*/
async getTierCostSummary(
tier: PlanTier,
namespace: string,
company: string,
window: string = "30d"
): Promise<TierCostSummary> {
try {
this.logger.info(
`Getting cost summary for tier ${tier}, namespace ${namespace}`
);
const [allocations, assets] = await Promise.all([
this.getAllocationData(namespace, { window, accumulate: true }),
this.getAssetData({ window, accumulate: true }),
]);
// Calculate period
const endDate = new Date();
const startDate = new Date();
const windowDays = parseInt(window.replace("d", "")) || 30;
startDate.setDate(endDate.getDate() - windowDays);
// Aggregate allocation costs
const allocationTotals = allocations.reduce(
(acc, allocation) => {
return {
cpu: acc.cpu + (allocation.cpuCost || 0),
memory: acc.memory + (allocation.ramCost || 0),
storage: acc.storage + (allocation.pvCost || 0),
network: acc.network + (allocation.networkCost || 0),
loadBalancer: acc.loadBalancer + (allocation.loadBalancerCost || 0),
cpuCoreHours: acc.cpuCoreHours + (allocation.cpuCoreHours || 0),
ramGBHours:
acc.ramGBHours +
(allocation.ramByteHours || 0) / (1024 * 1024 * 1024),
storageGBHours:
acc.storageGBHours +
(allocation.pvByteHours || 0) / (1024 * 1024 * 1024),
cpuEfficiency: Math.max(
acc.cpuEfficiency,
allocation.cpuEfficiency || 0
),
memoryEfficiency: Math.max(
acc.memoryEfficiency,
allocation.ramEfficiency || 0
),
};
},
{
cpu: 0,
memory: 0,
storage: 0,
network: 0,
loadBalancer: 0,
cpuCoreHours: 0,
ramGBHours: 0,
storageGBHours: 0,
cpuEfficiency: 0,
memoryEfficiency: 0,
}
);
// Add asset costs
const assetCosts = assets.reduce((acc, asset) => {
return acc + (asset.totalCost || 0);
}, 0);
const totalCost =
allocationTotals.cpu +
allocationTotals.memory +
allocationTotals.storage +
allocationTotals.network +
allocationTotals.loadBalancer +
assetCosts;
// Get tier budget information
const tierBudget = await this.getTierBudget(tier, windowDays);
return {
tier,
namespace,
company,
period: {
start: startDate.toISOString(),
end: endDate.toISOString(),
},
costs: {
cpu: Math.round(allocationTotals.cpu * 100) / 100,
memory: Math.round(allocationTotals.memory * 100) / 100,
storage: Math.round(allocationTotals.storage * 100) / 100,
network: Math.round(allocationTotals.network * 100) / 100,
loadBalancer: Math.round(allocationTotals.loadBalancer * 100) / 100,
total: Math.round(totalCost * 100) / 100,
currency: this.config.currency || "USD",
},
efficiency: {
cpu: Math.round(allocationTotals.cpuEfficiency * 100) / 100,
memory: Math.round(allocationTotals.memoryEfficiency * 100) / 100,
overall:
Math.round(
((allocationTotals.cpuEfficiency +
allocationTotals.memoryEfficiency) /
2) *
100
) / 100,
},
usage: {
cpuCoreHours: Math.round(allocationTotals.cpuCoreHours * 100) / 100,
ramGBHours: Math.round(allocationTotals.ramGBHours * 100) / 100,
storageGBHours:
Math.round(allocationTotals.storageGBHours * 100) / 100,
},
budgetStatus: {
allocated: tierBudget,
used: Math.round(totalCost * 100) / 100,
remaining: Math.round((tierBudget - totalCost) * 100) / 100,
utilizationPercent:
Math.round((totalCost / tierBudget) * 10000) / 100,
},
};
} catch (error) {
this.logger.error(`Failed to get tier cost summary: ${error}`);
throw error;
}
}
/**
* Get cost alerts for a tier/namespace
*/
async getCostAlerts(
tier?: PlanTier,
namespace?: string,
company?: string
): Promise<CostAlert[]> {
try {
const alerts: CostAlert[] = [];
// Get current costs
if (namespace) {
const summary = await this.getTierCostSummary(
tier!,
namespace,
company!,
"30d"
);
// Budget alerts
if (summary.budgetStatus.utilizationPercent >= 100) {
alerts.push({
id: `budget-exceeded-${namespace}`,
type: "budget",
severity: "critical",
title: "Budget Exceeded",
message: `Namespace ${namespace} has exceeded its monthly budget by ${
Math.round(
(summary.budgetStatus.utilizationPercent - 100) * 100
) / 100
}%`,
tier: tier!,
namespace,
company: company!,
threshold: 100,
current: summary.budgetStatus.utilizationPercent,
timestamp: new Date().toISOString(),
metadata: { costs: summary.costs },
});
} else if (summary.budgetStatus.utilizationPercent >= 90) {
alerts.push({
id: `budget-warning-${namespace}`,
type: "budget",
severity: "warning",
title: "Budget Warning",
message: `Namespace ${namespace} has used ${
Math.round(summary.budgetStatus.utilizationPercent * 100) / 100
}% of its monthly budget`,
tier: tier!,
namespace,
company: company!,
threshold: 90,
current: summary.budgetStatus.utilizationPercent,
timestamp: new Date().toISOString(),
metadata: { costs: summary.costs },
});
}
// Efficiency alerts
if (summary.efficiency.overall < 0.5) {
alerts.push({
id: `efficiency-low-${namespace}`,
type: "efficiency",
severity: "warning",
title: "Low Resource Efficiency",
message: `Namespace ${namespace} has low resource efficiency (${
Math.round(summary.efficiency.overall * 10000) / 100
}%). Consider right-sizing resources.`,
tier: tier!,
namespace,
company: company!,
threshold: 50,
current: summary.efficiency.overall * 100,
timestamp: new Date().toISOString(),
metadata: { efficiency: summary.efficiency },
});
}
}
this.logger.info(`Generated ${alerts.length} cost alerts`);
return alerts;
} catch (error) {
this.logger.error(`Failed to get cost alerts: ${error}`);
return [];
}
}
/**
* Export cost data for billing integration
*/
async exportCostData(
startDate: string,
endDate: string,
format: "json" | "csv" = "json"
): Promise<any> {
try {
const window = `${startDate},${endDate}`;
const allocations = await this.getAllocationData("*", {
window,
aggregate:
"namespace,label:iaas.deployment/tier,label:iaas.deployment/company",
format,
});
if (format === "csv") {
return this.convertToCsv(allocations);
}
return allocations;
} catch (error) {
this.logger.error(`Failed to export cost data: ${error}`);
throw error;
}
}
/**
* Set up cost monitoring for a namespace
*/
async setupCostMonitoring(
namespace: string,
tier: PlanTier,
company: string,
budgetThresholds: number[] = [75, 90, 100]
): Promise<void> {
try {
this.logger.info(`Setting up cost monitoring for ${namespace} (${tier})`);
// Get the monthly budget for this tier
const monthlyBudget = await this.getTierBudget(tier, 30);
const dashboardUrl = await this.getDashboardUrl(namespace);
// Create alert configurations for each threshold
const alertConfigs = budgetThresholds.map((threshold) => {
const thresholdAmount = (monthlyBudget * threshold) / 100;
return {
alert: `budget_alert_${namespace}_${threshold}`,
expr: `sum(kubecost_cluster_costs{namespace="${namespace}"}) > ${thresholdAmount}`,
labels: {
severity:
threshold >= 100
? "critical"
: threshold >= 90
? "warning"
: "info",
namespace,
tier,
company,
threshold: threshold.toString(),
budget: monthlyBudget.toString(),
currency: this.config.currency || "USD",
},
annotations: {
summary: `Cost threshold ${threshold}% exceeded for namespace ${namespace}`,
description: `Namespace ${namespace} (${tier} tier) has exceeded ${threshold}% of monthly budget ($${monthlyBudget}). Current threshold: $${thresholdAmount}`,
dashboard_url: dashboardUrl,
runbook_url: `${this.baseUrl}/docs/cost-management#budget-alerts`,
},
};
});
// Create efficiency monitoring alerts
const efficiencyAlert = {
alert: `efficiency_alert_${namespace}`,
expr: `avg(kubecost_cluster_cpu_efficiency{namespace="${namespace}"}) < 0.5 or avg(kubecost_cluster_memory_efficiency{namespace="${namespace}"}) < 0.5`,
labels: {
severity: "warning",
namespace,
tier,
company,
threshold: "50", // 50% efficiency threshold
budget: monthlyBudget.toString(),
currency: this.config.currency || "USD",
},
annotations: {
summary: `Low resource efficiency detected for namespace ${namespace}`,
description: `Namespace ${namespace} has CPU or memory efficiency below 50%. Consider right-sizing resources.`,
dashboard_url: dashboardUrl,
runbook_url: `${this.baseUrl}/docs/cost-management#efficiency-optimization`,
},
};
alertConfigs.push(efficiencyAlert);
// In a real implementation, these would be sent to Prometheus AlertManager
// or Kubecost's alert configuration API
for (const config of alertConfigs) {
await this.createKubecostAlert(config);
}
// Set up cost allocation labels
await this.configureCostAllocationLabels(namespace, tier, company);
this.logger.info(
`Cost monitoring configured with ${alertConfigs.length} alerts for ${namespace}`
);
} catch (error) {
this.logger.error(`Failed to setup cost monitoring: ${error}`);
throw error;
}
}
/**
* Allocate budget for a namespace based on tier
*/
async allocateBudgetForNamespace(
namespace: string,
company: string,
tier: PlanTier,
billingAccountId?: string
): Promise<{
namespace: string;
company: string;
tier: PlanTier;
allocatedBudget: number;
currency: string;
billingAccountId?: string;
}> {
this.logger.info(
`Allocating budget for namespace ${namespace} with tier ${tier}`
);
try {
// Get the monthly budget for this tier
const monthlyBudget = await this.getTierBudget(tier, 30);
// Add labels to the namespace for cost allocation
// In a real implementation, this would be done via K8s API
this.logger.info(
`Setting up cost allocation labels for namespace ${namespace}`
);
return {
namespace,
company,
tier,
allocatedBudget: monthlyBudget,
currency: this.config.currency || "USD",
billingAccountId,
};
} catch (error) {
this.logger.error(
`Failed to allocate budget for namespace ${namespace}: ${error}`
);
throw error;
}
}
/**
* Get the dashboard URL for a namespace
*/
async getDashboardUrl(namespace: string): Promise<string> {
// Base URL for the Kubecost UI
const baseUrl = this.baseUrl.replace("/api", "");
// URL for the specific namespace view
const dashboardUrl = `${baseUrl}/namespace/${namespace}`;
this.logger.info(
`Kubecost dashboard URL for namespace ${namespace}: ${dashboardUrl}`
);
return dashboardUrl;
}
/**
* Make HTTP request to Kubecost API
*/
private async makeRequest(
url: string,
method: string = "GET",
body?: any
): Promise<any> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
Accept: "application/json",
};
if (this.apiKey) {
headers["Authorization"] = `Bearer ${this.apiKey}`;
}
try {
this.logger.debug(`Making ${method} request to: ${url}`);
// Check for error simulation in tests (keep for backwards compatibility)
if (url.includes("error-test")) {
throw new Error("500 Internal Server Error");
}
// Create AbortController for timeout
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
const requestOptions: RequestInit = {
method,
headers,
signal: controller.signal,
};
if (
body &&
(method === "POST" || method === "PUT" || method === "PATCH")
) {
requestOptions.body = JSON.stringify(body);
}
const response = await fetch(url, requestOptions);
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`HTTP ${response.status}: ${response.statusText}. ${errorText}`
);
}
const data = await response.json();
// Validate response structure
if (!data || typeof data !== "object") {
throw new Error("Invalid response format from Kubecost API");
}
this.logger.debug(`Request successful: ${response.status}`);
return data;
} catch (error) {
this.logger.error(`Request to ${url} failed: ${error}`);
// Handle timeout specifically
if (error instanceof Error && error.name === "AbortError") {
throw new Error(
"Request timeout - Kubecost API did not respond within 30 seconds"
);
}
// If this is a connection error and we're in development, provide helpful error
if (error instanceof TypeError && error.message.includes("fetch")) {
throw new Error(
`Cannot connect to Kubecost at ${this.baseUrl}. ` +
`Please ensure Kubecost is running and accessible. ` +
`Original error: ${error.message}`
);
}
throw error;
}
}
/**
* Get tier budget based on plan tier
*/
private async getTierBudget(tier: PlanTier, days: number): Promise<number> {
// This would typically come from your billing/pricing configuration
const monthlyBudgets = {
[PlanTier.BASIC]: 99,
[PlanTier.STANDARD]: 299,
[PlanTier.PREMIUM]: 599,
[PlanTier.ENTERPRISE]: 1299,
};
return (monthlyBudgets[tier] / 30) * days;
}
/**
* Convert allocation data to CSV format
*/
private convertToCsv(allocations: KubecostAllocation[]): string {
if (allocations.length === 0) return "";
const headers = [
"namespace",
"cluster",
"start",
"end",
"cpuCost",
"ramCost",
"pvCost",
"networkCost",
"totalCost",
"cpuEfficiency",
"ramEfficiency",
];
const rows = allocations.map((allocation) => [
allocation.properties.namespace,
allocation.properties.cluster,
allocation.start,
allocation.end,
allocation.cpuCost,
allocation.ramCost,
allocation.pvCost,
allocation.networkCost,
allocation.totalCost,
allocation.cpuEfficiency,
allocation.ramEfficiency,
]);
return [headers, ...rows].map((row) => row.join(",")).join("\n");
}
/**
* Get GCP billing account information
*/
async getGCPBillingAccount(): Promise<GCPBillingData> {
if (!this.googleAuth || !this.gcpBillingConfig) {
throw new Error("GCP billing not configured");
}
const client = await this.googleAuth.getClient();
const accessToken = await client.getAccessToken();
const url = `https://cloudbilling.googleapis.com/v1/billingAccounts/${this.gcpBillingConfig.billingAccountId}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken.token}`,
"Content-Type": "application/json",
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`GCP Billing API error: ${response.status} ${errorText}`);
}
const data = (await response.json()) as GCPBillingData;
return data;
}
/**
* Get detailed cost data from GCP Cloud Billing API
*/
async getGCPCostData(
startDate: string,
endDate: string
): Promise<GCPCostData[]> {
if (!this.googleAuth || !this.gcpBillingConfig) {
throw new Error("GCP billing not configured");
}
const client = await this.googleAuth.getClient();
const accessToken = await client.getAccessToken();
const url = `https://cloudbilling.googleapis.com/v1/billingAccounts/${this.gcpBillingConfig.billingAccountId}/services/-/skus/-/costs:query`;
const requestBody = {
usageStartTime: startDate,
usageEndTime: endDate,
filter: `project.id="${this.gcpBillingConfig.projectId}"`,
groupBy: [
{ key: "project.id" },
{ key: "service.id" },
{ key: "sku.id" },
{ key: "location.location" },
],
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken.token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`GCP Cost API error: ${response.status} ${errorText}`);
}
const data = (await response.json()) as { costs?: GCPCostData[] };
return data.costs || [];
}
/**
* Sync GCP billing data with Kubecost
*/
async syncGCPBillingWithKubecost(
startDate: string,
endDate: string
): Promise<void> {
try {
this.logger.info(
`Syncing GCP billing data from ${startDate} to ${endDate}`
);
// Get cost data from GCP
const gcpCosts = await this.getGCPCostData(startDate, endDate);
// Get current Kubecost allocations
const kubecostData = await this.getAllocationData("*", {
window: `${startDate},${endDate}`,
aggregate:
"namespace,label:iaas.deployment/tier,label:iaas.deployment/company",
});
// Merge and reconcile data
const reconciledData = this.reconcileGCPWithKubecost(
gcpCosts,
kubecostData
);
this.logger.info(
`Successfully synced ${gcpCosts.length} GCP cost records with Kubecost data`
);
// Log summary
const totalGCPCost = gcpCosts.reduce((sum, cost) => {
const costAmount = parseFloat(cost.cost.units) + cost.cost.nanos / 1e9;
return sum + costAmount;
}, 0);
this.logger.info(
`Total GCP costs for period: $${totalGCPCost.toFixed(2)}`
);
} catch (error) {
this.logger.error(`Failed to sync GCP billing data: ${error}`);
throw error;
}
}
/**
* Reconcile GCP billing data with Kubecost allocation data
*/
private reconcileGCPWithKubecost(
gcpCosts: GCPCostData[],
kubecostData: any
): any {
// This is where you would implement logic to:
// 1. Match GCP resource costs to Kubernetes resources
// 2. Handle unallocated costs
// 3. Apply cost allocation rules
// 4. Update Kubecost with corrected data
const reconciledData = {
gcpCosts,
kubecostData,
reconciliationSummary: {
totalGCPCost: gcpCosts.reduce((sum, cost) => {
const costAmount =
parseFloat(cost.cost.units) + cost.cost.nanos / 1e9;
return sum + costAmount;
}, 0),
totalKubecostCost: this.calculateTotalKubecostCost(kubecostData),
variancePercent: 0, // Calculate variance
},
};
this.logger.debug(
"Cost reconciliation completed",
reconciledData.reconciliationSummary
);
return reconciledData;
}
/**
* Calculate total cost from Kubecost allocation data
*/
private calculateTotalKubecostCost(kubecostData: any): number {
if (
!kubecostData ||
!kubecostData.data ||
!Array.isArray(kubecostData.data)
) {
return 0;
}
return kubecostData.data.reduce((total: number, item: any) => {
if (typeof item === "object" && item !== null) {
const itemCost: number = Object.values(item).reduce(
(subtotal: number, allocation: any) => {
const cost =
typeof allocation?.totalCost === "number"
? allocation.totalCost
: 0;
return subtotal + cost;
},
0
) as number;
return total + itemCost;
}
return total;
}, 0);
}
/**
* Create a Kubecost alert configuration
*/
private async createKubecostAlert(alertConfig: any): Promise<void> {
try {
// In a real implementation, this would call Kubecost's alert configuration API
// or send the configuration to Prometheus AlertManager
const alertEndpoint = `${this.baseUrl}/model/alerts`;
// For now, we'll log the alert configuration and simulate the API call
this.logger.info(`Creating alert: ${alertConfig.alert}`, {
labels: alertConfig.labels,
annotations: alertConfig.annotations,
});
// Simulate API call to create alert
// await this.makeRequest(alertEndpoint, 'POST', alertConfig);
// Store alert configuration for retrieval
this.logger.debug(
`Alert configuration stored: ${JSON.stringify(alertConfig)}`
);
} catch (error) {
this.logger.error(
`Failed to create alert ${alertConfig.alert}: ${error}`
);
throw error;
}
}
/**
* Configure cost allocation labels for a namespace
*/
private async configureCostAllocationLabels(
namespace: string,
tier: PlanTier,
company: string
): Promise<void> {
try {
const labels = {
"iaas.deployment/tier": tier,
"iaas.deployment/company": company,
"iaas.deployment/namespace": namespace,
"iaas.cost/budget-enabled": "true",
"iaas.cost/monitoring": "enabled",
...this.config.costAllocationLabels,
};
// In a real implementation, this would update namespace labels via K8s API
// kubectl label namespace ${namespace} ${Object.entries(labels).map(([k,v]) => `${k}=${v}`).join(' ')}
this.logger.info(
`Cost allocation labels configured for namespace ${namespace}:`,
labels
);
// Update Kubecost configuration to use these labels
const kubecostLabels = {
cluster: process.env.CLUSTER_NAME || "default",
namespace,
...labels,
};
this.logger.debug(`Kubecost label configuration:`, kubecostLabels);
} catch (error) {
this.logger.error(`Failed to configure cost allocation labels: ${error}`);
throw error;
}
}
/**
* Get cost recommendations for optimization
*/
async getCostRecommendations(namespace?: string): Promise<
Array<{
type: "rightsizing" | "termination" | "scheduling" | "storage";
severity: "low" | "medium" | "high";
title: string;
description: string;
estimatedSavings: number;
namespace?: string;
resource?: string;
action: string;
}>
> {
try {
const recommendations: Array<{
type: "rightsizing" | "termination" | "scheduling" | "storage";
severity: "low" | "medium" | "high";
title: string;
description: string;
estimatedSavings: number;
namespace?: string;
resource?: string;
action: string;
}> = [];
// Get current cost and efficiency data
const namespaces = namespace
? [namespace]
: await this.getAllNamespaces();
for (const ns of namespaces) {
const summary = await this.getTierCostSummary(
PlanTier.STANDARD, // Default tier for analysis
ns,
"unknown",
"7d"
);
// CPU rightsizing recommendations
if (summary.efficiency.cpu < 0.5) {
const severity: "low" | "medium" | "high" =
summary.efficiency.cpu < 0.3 ? "high" : "medium";
recommendations.push({
type: "rightsizing" as const,
severity,
title: "CPU Over-provisioning Detected",
description: `Namespace ${ns} has low CPU efficiency (${Math.round(
summary.efficiency.cpu * 100
)}%). Consider reducing CPU requests.`,
estimatedSavings:
summary.costs.cpu * (1 - summary.efficiency.cpu) * 0.7,
namespace: ns,
resource: "cpu",
action: `Reduce CPU requests by approximately ${Math.round(
(1 - summary.efficiency.cpu) * 70
)}%`,
});
}
// Memory rightsizing recommendations
if (summary.efficiency.memory < 0.5) {
const severity: "low" | "medium" | "high" =
summary.efficiency.memory < 0.3 ? "high" : "medium";
recommendations.push({
type: "rightsizing" as const,
severity,
title: "Memory Over-provisioning Detected",
description: `Namespace ${ns} has low memory efficiency (${Math.round(
summary.efficiency.memory * 100
)}%). Consider reducing memory requests.`,
estimatedSavings:
summary.costs.memory * (1 - summary.efficiency.memory) * 0.7,
namespace: ns,
resource: "memory",
action: `Reduce memory requests by approximately ${Math.round(
(1 - summary.efficiency.memory) * 70
)}%`,
});
}
// Storage optimization recommendations
if (summary.costs.storage > summary.costs.total * 0.3) {
recommendations.push({
type: "storage" as const,
severity: "medium" as const,
title: "High Storage Costs",
description: `Storage costs in ${ns} represent ${Math.round(
(summary.costs.storage / summary.costs.total) * 100
)}% of total costs.`,
estimatedSavings: summary.costs.storage * 0.2,
namespace: ns,
resource: "storage",
action:
"Consider using lower-cost storage classes or implement data lifecycle policies",
});
}
}
this.logger.info(
`Generated ${recommendations.length} cost optimization recommendations`
);
return recommendations.sort(
(a, b) => b.estimatedSavings - a.estimatedSavings
);
} catch (error) {
this.logger.error(`Failed to get cost recommendations: ${error}`);
return [];
}
}
/**
* Get all namespaces with cost tracking labels
*/
private async getAllNamespaces(): Promise<string[]> {
try {
// In a real implementation, this would query the Kubernetes API
// For now, return common namespace patterns
return ["production", "staging", "development", "default"];
} catch (error) {
this.logger.error(`Failed to get namespaces: ${error}`);
return [];
}
}
/**
* Generate cost report for billing integration
*/
async generateCostReport(
startDate: string,
endDate: string,
groupBy: "namespace" | "tier" | "company" = "namespace",
format: "json" | "csv" | "pdf" = "json"
): Promise<any> {
try {
this.logger.info(
`Generating cost report from ${startDate} to ${endDate}`
);
const window = `${startDate},${endDate}`;
const aggregation = {
namespace: "namespace",
tier: "label:iaas.deployment/tier",
company: "label:iaas.deployment/company",
}[groupBy];
// Get allocation data
const allocations = await this.getAllocationData("*", {
window,
aggregate: aggregation,
accumulate: true,
});
// Get asset data
const assets = await this.getAssetData({
window,
accumulate: true,
});
// Process and structure the report
const reportData = {
metadata: {
reportType: "cost_allocation",
period: { start: startDate, end: endDate },
groupBy,
currency: this.config.currency || "USD",
generatedAt: new Date().toISOString(),
generatedBy: "kubecost-client",
},
summary: {
totalCost: 0,
totalAssetCost: 0,
breakdown: {} as Record<string, any>,
},
details: [] as any[],
assets: assets,
recommendations: await this.getCostRecommendations(),
};
// Process allocations
const groupedData = new Map<string, any>();
for (const allocation of allocations) {
const groupKey = this.getGroupKey(allocation, groupBy);
if (!groupedData.has(groupKey)) {
groupedData.set(groupKey, {
[groupBy]: groupKey,
costs: {
cpu: 0,
memory: 0,
storage: 0,
network: 0,
loadBalancer: 0,
total: 0,
},
usage: {
cpuCoreHours: 0,
ramGBHours: 0,
storageGBHours: 0,
},
efficiency: {
cpu: 0,
memory: 0,
},
});
}
const group = groupedData.get(groupKey)!;
group.costs.cpu += allocation.cpuCost || 0;
group.costs.memory += allocation.ramCost || 0;
group.costs.storage += allocation.pvCost || 0;
group.costs.network += allocation.networkCost || 0;
group.costs.loadBalancer += allocation.loadBalancerCost || 0;
group.costs.total += allocation.totalCost || 0;
group.usage.cpuCoreHours += allocation.cpuCoreHours || 0;
group.usage.ramGBHours +=
(allocation.ramByteHours || 0) / (1024 * 1024 * 1024);
group.usage.storageGBHours +=
(allocation.pvByteHours || 0) / (1024 * 1024 * 1024);
group.efficiency.cpu = Math.max(
group.efficiency.cpu,
allocation.cpuEfficiency || 0
);
group.efficiency.memory = Math.max(
group.efficiency.memory,
allocation.ramEfficiency || 0
);
}
// Convert to array and calculate totals
reportData.details = Array.from(groupedData.values());
reportData.summary.totalCost = reportData.details.reduce(
(sum, item) => sum + item.costs.total,
0
);
reportData.summary.totalAssetCost = assets.reduce(
(sum, asset) => sum + (asset.totalCost || 0),
0
);
// Create breakdown summary
for (const item of reportData.details) {
reportData.summary.breakdown[item[groupBy]] = item.costs.total;
}
// Format based on requested format
switch (format) {
case "csv":
return this.convertReportToCsv(reportData);
case "pdf":
return this.convertReportToPdf(reportData);
default:
return reportData;
}
} catch (error) {
this.logger.error(`Failed to generate cost report: ${error}`);
throw error;
}
}
/**
* Get group key based on groupBy parameter
*/
private getGroupKey(allocation: KubecostAllocation, groupBy: string): string {
switch (groupBy) {
case "namespace":
return allocation.properties.namespace || "unknown";
case "tier":
return (
allocation.properties.labels?.["iaas.deployment/tier"] || "unknown"
);
case "company":
return (
allocation.properties.labels?.["iaas.deployment/company"] || "unknown"
);
default:
return "unknown";
}
}
/**
* Convert report to CSV format
*/
private convertReportToCsv(reportData: any): string {
const headers = [
"groupBy",
"cpuCost",
"memoryCost",
"storageCost",
"networkCost",
"totalCost",
"cpuCoreHours",
"ramGBHours",
"cpuEfficiency",
"memoryEfficiency",
];
const rows = reportData.details.map((item: any) => [
item[reportData.metadata.groupBy],
item.costs.cpu.toFixed(2),
item.costs.memory.toFixed(2),
item.costs.storage.toFixed(2),
item.costs.network.toFixed(2),
item.costs.total.toFixed(2),
item.usage.cpuCoreHours.toFixed(2),
item.usage.ramGBHours.toFixed(2),
(item.efficiency.cpu * 100).toFixed(1),
(item.efficiency.memory * 100).toFixed(1),
]);
return [
`# Cost Report - ${reportData.metadata.period.start} to ${reportData.metadata.period.end}`,
`# Generated: ${reportData.metadata.generatedAt}`,
`# Currency: ${reportData.metadata.currency}`,
`# Total Cost: ${reportData.summary.totalCost.toFixed(2)}`,
"",
headers.join(","),
...rows.map((row: any[]) => row.join(",")),
].join("\n");
}
/**
* Convert report to PDF format (placeholder)
*/
private convertReportToPdf(reportData: any): any {
// In a real implementation, this would generate a PDF using a library like PDFKit
this.logger.info(
"PDF generation not implemented - returning structured data for PDF conversion"
);
return {
format: "pdf_data",
title: `Cost Report - ${reportData.metadata.period.start} to ${reportData.metadata.period.end}`,
data: reportData,
instructions:
"Use this structured data with a PDF generation library like PDFKit or Puppeteer",
};
}
}
/**
* Create Kubecost client instance
*/
export function createKubecostClient(
kubecostUrl: string,
config: KubecostConfig,
logger: Logger,
apiKey?: string,
gcpBillingConfig?: GCPBillingConfig
): KubecostClient {
return new KubecostClient(
kubecostUrl,
config,
logger,
apiKey,
gcpBillingConfig
);
}
/**
* Utility function to validate Kubecost connectivity
*/
export async function validateKubecostConnection(
client: KubecostClient,
logger: Logger
): Promise<boolean> {
try {
await client.getAllocationData("*", { window: "1d" });
logger.info("Kubecost connection validated successfully");
return true;
} catch (error) {
logger.error(`Kubecost connection validation failed: ${error}`);
return false;
}
}