UNPKG

@chimoney.io/iaas-k8s-deployment

Version:

Kubernetes Infrastructure as a Service deployment package for streamlined multi-cloud environments

963 lines 41.9 kB
/** * 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