@chimoney.io/iaas-k8s-deployment
Version:
Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments
963 lines • 41.9 kB
JavaScript
/**
* 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 { PlanTier } from "../types/plans.js";
import { GoogleAuth } from "google-auth-library";
export class KubecostClient {
constructor(baseUrl, config, logger, apiKey, 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
*/
async initializeGCPAuth() {
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, options = { window: "7d" }) {
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] || {});
}
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 = { window: "7d" }) {
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] || {});
}
catch (error) {
this.logger.error(`Failed to get asset data: ${error}`);
throw error;
}
}
/**
* Get comprehensive cost summary for a tier/namespace
*/
async getTierCostSummary(tier, namespace, company, window = "30d") {
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, namespace, company) {
try {
const alerts = [];
// 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, endDate, format = "json") {
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, tier, company, budgetThresholds = [75, 90, 100]) {
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, company, tier, billingAccountId) {
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) {
// 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
*/
async makeRequest(url, method = "GET", body) {
const headers = {
"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 = {
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
*/
async getTierBudget(tier, days) {
// 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
*/
convertToCsv(allocations) {
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() {
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());
return data;
}
/**
* Get detailed cost data from GCP Cloud Billing API
*/
async getGCPCostData(startDate, endDate) {
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());
return data.costs || [];
}
/**
* Sync GCP billing data with Kubecost
*/
async syncGCPBillingWithKubecost(startDate, endDate) {
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
*/
reconcileGCPWithKubecost(gcpCosts, kubecostData) {
// 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
*/
calculateTotalKubecostCost(kubecostData) {
if (!kubecostData ||
!kubecostData.data ||
!Array.isArray(kubecostData.data)) {
return 0;
}
return kubecostData.data.reduce((total, item) => {
if (typeof item === "object" && item !== null) {
const itemCost = Object.values(item).reduce((subtotal, allocation) => {
const cost = typeof allocation?.totalCost === "number"
? allocation.totalCost
: 0;
return subtotal + cost;
}, 0);
return total + itemCost;
}
return total;
}, 0);
}
/**
* Create a Kubecost alert configuration
*/
async createKubecostAlert(alertConfig) {
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
*/
async configureCostAllocationLabels(namespace, tier, company) {
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) {
try {
const recommendations = [];
// 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 = summary.efficiency.cpu < 0.3 ? "high" : "medium";
recommendations.push({
type: "rightsizing",
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 = summary.efficiency.memory < 0.3 ? "high" : "medium";
recommendations.push({
type: "rightsizing",
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",
severity: "medium",
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
*/
async getAllNamespaces() {
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, endDate, groupBy = "namespace", format = "json") {
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: {},
},
details: [],
assets: assets,
recommendations: await this.getCostRecommendations(),
};
// Process allocations
const groupedData = new Map();
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
*/
getGroupKey(allocation, groupBy) {
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
*/
convertReportToCsv(reportData) {
const headers = [
"groupBy",
"cpuCost",
"memoryCost",
"storageCost",
"networkCost",
"totalCost",
"cpuCoreHours",
"ramGBHours",
"cpuEfficiency",
"memoryEfficiency",
];
const rows = reportData.details.map((item) => [
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) => row.join(",")),
].join("\n");
}
/**
* Convert report to PDF format (placeholder)
*/
convertReportToPdf(reportData) {
// 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, config, logger, apiKey, gcpBillingConfig) {
return new KubecostClient(kubecostUrl, config, logger, apiKey, gcpBillingConfig);
}
/**
* Utility function to validate Kubecost connectivity
*/
export async function validateKubecostConnection(client, logger) {
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;
}
}
//# sourceMappingURL=kubecost-client.js.map