infra-cost 0.3.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +663 -105
- package/dist/cli/index.js +10888 -0
- package/dist/index.js +10072 -35530
- package/package.json +45 -28
- package/dist/demo/test-enhanced-ui.js +0 -1693
- package/dist/demo/test-multi-cloud-dashboard.js +0 -3375
|
@@ -1,3375 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
var __create = Object.create;
|
|
3
|
-
var __defProp = Object.defineProperty;
|
|
4
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var __copyProps = (to, from, except, desc) => {
|
|
9
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
-
for (let key of __getOwnPropNames(from))
|
|
11
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
-
}
|
|
14
|
-
return to;
|
|
15
|
-
};
|
|
16
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
-
mod
|
|
23
|
-
));
|
|
24
|
-
|
|
25
|
-
// src/visualization/multi-cloud-dashboard.ts
|
|
26
|
-
var import_chalk4 = __toESM(require("chalk"));
|
|
27
|
-
|
|
28
|
-
// src/types/providers.ts
|
|
29
|
-
var CloudProviderAdapter = class {
|
|
30
|
-
constructor(config) {
|
|
31
|
-
this.config = config;
|
|
32
|
-
}
|
|
33
|
-
calculateServiceTotals(rawCostData) {
|
|
34
|
-
return this.processRawCostData(rawCostData);
|
|
35
|
-
}
|
|
36
|
-
processRawCostData(rawCostData) {
|
|
37
|
-
const totals = {
|
|
38
|
-
lastMonth: 0,
|
|
39
|
-
thisMonth: 0,
|
|
40
|
-
last7Days: 0,
|
|
41
|
-
yesterday: 0
|
|
42
|
-
};
|
|
43
|
-
const totalsByService = {
|
|
44
|
-
lastMonth: {},
|
|
45
|
-
thisMonth: {},
|
|
46
|
-
last7Days: {},
|
|
47
|
-
yesterday: {}
|
|
48
|
-
};
|
|
49
|
-
const now = /* @__PURE__ */ new Date();
|
|
50
|
-
const startOfThisMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
|
51
|
-
const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
|
52
|
-
const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0);
|
|
53
|
-
const yesterday = new Date(now);
|
|
54
|
-
yesterday.setDate(yesterday.getDate() - 1);
|
|
55
|
-
const last7DaysStart = new Date(now);
|
|
56
|
-
last7DaysStart.setDate(last7DaysStart.getDate() - 7);
|
|
57
|
-
for (const [serviceName, serviceCosts] of Object.entries(rawCostData)) {
|
|
58
|
-
let lastMonthServiceTotal = 0;
|
|
59
|
-
let thisMonthServiceTotal = 0;
|
|
60
|
-
let last7DaysServiceTotal = 0;
|
|
61
|
-
let yesterdayServiceTotal = 0;
|
|
62
|
-
for (const [dateStr, cost] of Object.entries(serviceCosts)) {
|
|
63
|
-
const date = new Date(dateStr);
|
|
64
|
-
if (date >= startOfLastMonth && date <= endOfLastMonth) {
|
|
65
|
-
lastMonthServiceTotal += cost;
|
|
66
|
-
}
|
|
67
|
-
if (date >= startOfThisMonth) {
|
|
68
|
-
thisMonthServiceTotal += cost;
|
|
69
|
-
}
|
|
70
|
-
if (date >= last7DaysStart && date < yesterday) {
|
|
71
|
-
last7DaysServiceTotal += cost;
|
|
72
|
-
}
|
|
73
|
-
if (date.toDateString() === yesterday.toDateString()) {
|
|
74
|
-
yesterdayServiceTotal += cost;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
totalsByService.lastMonth[serviceName] = lastMonthServiceTotal;
|
|
78
|
-
totalsByService.thisMonth[serviceName] = thisMonthServiceTotal;
|
|
79
|
-
totalsByService.last7Days[serviceName] = last7DaysServiceTotal;
|
|
80
|
-
totalsByService.yesterday[serviceName] = yesterdayServiceTotal;
|
|
81
|
-
totals.lastMonth += lastMonthServiceTotal;
|
|
82
|
-
totals.thisMonth += thisMonthServiceTotal;
|
|
83
|
-
totals.last7Days += last7DaysServiceTotal;
|
|
84
|
-
totals.yesterday += yesterdayServiceTotal;
|
|
85
|
-
}
|
|
86
|
-
return {
|
|
87
|
-
totals,
|
|
88
|
-
totalsByService
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
var ResourceType = /* @__PURE__ */ ((ResourceType2) => {
|
|
93
|
-
ResourceType2["COMPUTE"] = "compute";
|
|
94
|
-
ResourceType2["STORAGE"] = "storage";
|
|
95
|
-
ResourceType2["DATABASE"] = "database";
|
|
96
|
-
ResourceType2["NETWORK"] = "network";
|
|
97
|
-
ResourceType2["SECURITY"] = "security";
|
|
98
|
-
ResourceType2["SERVERLESS"] = "serverless";
|
|
99
|
-
ResourceType2["CONTAINER"] = "container";
|
|
100
|
-
ResourceType2["ANALYTICS"] = "analytics";
|
|
101
|
-
return ResourceType2;
|
|
102
|
-
})(ResourceType || {});
|
|
103
|
-
|
|
104
|
-
// src/providers/aws.ts
|
|
105
|
-
var import_client_cost_explorer = require("@aws-sdk/client-cost-explorer");
|
|
106
|
-
var import_client_iam = require("@aws-sdk/client-iam");
|
|
107
|
-
var import_client_sts = require("@aws-sdk/client-sts");
|
|
108
|
-
var import_client_ec2 = require("@aws-sdk/client-ec2");
|
|
109
|
-
var import_client_s3 = require("@aws-sdk/client-s3");
|
|
110
|
-
var import_client_rds = require("@aws-sdk/client-rds");
|
|
111
|
-
var import_client_lambda = require("@aws-sdk/client-lambda");
|
|
112
|
-
var import_client_budgets = require("@aws-sdk/client-budgets");
|
|
113
|
-
var import_dayjs = __toESM(require("dayjs"));
|
|
114
|
-
|
|
115
|
-
// src/logger.ts
|
|
116
|
-
var import_chalk = __toESM(require("chalk"));
|
|
117
|
-
var import_ora = __toESM(require("ora"));
|
|
118
|
-
var spinner;
|
|
119
|
-
function showSpinner(text) {
|
|
120
|
-
if (!spinner) {
|
|
121
|
-
spinner = (0, import_ora.default)({ text: "" }).start();
|
|
122
|
-
}
|
|
123
|
-
spinner.text = text;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// src/analytics/anomaly-detector.ts
|
|
127
|
-
var AnomalyDetector = class {
|
|
128
|
-
constructor(config = { sensitivity: "MEDIUM", lookbackPeriods: 14 }) {
|
|
129
|
-
this.config = config;
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* Detects anomalies using multiple statistical methods
|
|
133
|
-
*/
|
|
134
|
-
detectAnomalies(dataPoints) {
|
|
135
|
-
if (dataPoints.length < this.config.lookbackPeriods) {
|
|
136
|
-
return [];
|
|
137
|
-
}
|
|
138
|
-
const anomalies = [];
|
|
139
|
-
anomalies.push(...this.detectStatisticalAnomalies(dataPoints));
|
|
140
|
-
anomalies.push(...this.detectTrendAnomalies(dataPoints));
|
|
141
|
-
anomalies.push(...this.detectSeasonalAnomalies(dataPoints));
|
|
142
|
-
return this.consolidateAnomalies(anomalies);
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Statistical anomaly detection using modified Z-score
|
|
146
|
-
*/
|
|
147
|
-
detectStatisticalAnomalies(dataPoints) {
|
|
148
|
-
const anomalies = [];
|
|
149
|
-
const values = dataPoints.map((dp) => dp.value);
|
|
150
|
-
for (let i = this.config.lookbackPeriods; i < dataPoints.length; i++) {
|
|
151
|
-
const currentValue = values[i];
|
|
152
|
-
const historicalValues = values.slice(i - this.config.lookbackPeriods, i);
|
|
153
|
-
const median = this.calculateMedian(historicalValues);
|
|
154
|
-
const mad = this.calculateMAD(historicalValues, median);
|
|
155
|
-
const modifiedZScore = mad === 0 ? 0 : 0.6745 * (currentValue - median) / mad;
|
|
156
|
-
const threshold = this.getSensitivityThreshold();
|
|
157
|
-
if (Math.abs(modifiedZScore) > threshold) {
|
|
158
|
-
const deviation = currentValue - median;
|
|
159
|
-
const deviationPercentage = median === 0 ? 0 : Math.abs(deviation / median) * 100;
|
|
160
|
-
anomalies.push({
|
|
161
|
-
timestamp: dataPoints[i].timestamp,
|
|
162
|
-
actualValue: currentValue,
|
|
163
|
-
expectedValue: median,
|
|
164
|
-
deviation: Math.abs(deviation),
|
|
165
|
-
deviationPercentage,
|
|
166
|
-
severity: this.calculateSeverity(Math.abs(modifiedZScore), threshold),
|
|
167
|
-
confidence: Math.min(95, Math.abs(modifiedZScore) / threshold * 100),
|
|
168
|
-
type: deviation > 0 ? "SPIKE" : "DROP",
|
|
169
|
-
description: this.generateAnomalyDescription("STATISTICAL", deviation, deviationPercentage),
|
|
170
|
-
potentialCauses: this.generatePotentialCauses(deviation > 0 ? "SPIKE" : "DROP", deviationPercentage)
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return anomalies;
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Trend-based anomaly detection
|
|
178
|
-
*/
|
|
179
|
-
detectTrendAnomalies(dataPoints) {
|
|
180
|
-
const anomalies = [];
|
|
181
|
-
const values = dataPoints.map((dp) => dp.value);
|
|
182
|
-
const trendWindow = Math.min(7, Math.floor(this.config.lookbackPeriods / 2));
|
|
183
|
-
for (let i = trendWindow * 2; i < dataPoints.length; i++) {
|
|
184
|
-
const recentTrend = this.calculateLinearTrend(values.slice(i - trendWindow, i));
|
|
185
|
-
const historicalTrend = this.calculateLinearTrend(values.slice(i - trendWindow * 2, i - trendWindow));
|
|
186
|
-
const trendChange = Math.abs(recentTrend - historicalTrend);
|
|
187
|
-
const trendChangeThreshold = this.config.sensitivity === "HIGH" ? 0.1 : this.config.sensitivity === "MEDIUM" ? 0.2 : 0.3;
|
|
188
|
-
if (trendChange > trendChangeThreshold) {
|
|
189
|
-
const currentValue = values[i];
|
|
190
|
-
const expectedValue = values[i - 1] + historicalTrend;
|
|
191
|
-
const deviation = Math.abs(currentValue - expectedValue);
|
|
192
|
-
const deviationPercentage = expectedValue === 0 ? 0 : deviation / expectedValue * 100;
|
|
193
|
-
anomalies.push({
|
|
194
|
-
timestamp: dataPoints[i].timestamp,
|
|
195
|
-
actualValue: currentValue,
|
|
196
|
-
expectedValue,
|
|
197
|
-
deviation,
|
|
198
|
-
deviationPercentage,
|
|
199
|
-
severity: this.calculateSeverity(trendChange, trendChangeThreshold),
|
|
200
|
-
confidence: Math.min(90, trendChange / trendChangeThreshold * 100),
|
|
201
|
-
type: "TREND_CHANGE",
|
|
202
|
-
description: `Significant trend change detected: ${recentTrend > historicalTrend ? "acceleration" : "deceleration"} in cost growth`,
|
|
203
|
-
potentialCauses: this.generatePotentialCauses("TREND_CHANGE", deviationPercentage)
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
return anomalies;
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Seasonal anomaly detection
|
|
211
|
-
*/
|
|
212
|
-
detectSeasonalAnomalies(dataPoints) {
|
|
213
|
-
if (!this.config.seasonalityPeriods || dataPoints.length < this.config.seasonalityPeriods * 2) {
|
|
214
|
-
return [];
|
|
215
|
-
}
|
|
216
|
-
const anomalies = [];
|
|
217
|
-
const values = dataPoints.map((dp) => dp.value);
|
|
218
|
-
const seasonalPeriod = this.config.seasonalityPeriods;
|
|
219
|
-
for (let i = seasonalPeriod; i < dataPoints.length; i++) {
|
|
220
|
-
const currentValue = values[i];
|
|
221
|
-
const seasonalBaseline = values[i - seasonalPeriod];
|
|
222
|
-
const seasonalDeviation = Math.abs(currentValue - seasonalBaseline);
|
|
223
|
-
const seasonalDeviationPercentage = seasonalBaseline === 0 ? 0 : seasonalDeviation / seasonalBaseline * 100;
|
|
224
|
-
const threshold = seasonalBaseline * 0.3;
|
|
225
|
-
if (seasonalDeviation > threshold && seasonalDeviationPercentage > 25) {
|
|
226
|
-
anomalies.push({
|
|
227
|
-
timestamp: dataPoints[i].timestamp,
|
|
228
|
-
actualValue: currentValue,
|
|
229
|
-
expectedValue: seasonalBaseline,
|
|
230
|
-
deviation: seasonalDeviation,
|
|
231
|
-
deviationPercentage: seasonalDeviationPercentage,
|
|
232
|
-
severity: this.calculateSeverity(seasonalDeviationPercentage, 25),
|
|
233
|
-
confidence: 80,
|
|
234
|
-
type: "SEASONAL_ANOMALY",
|
|
235
|
-
description: `Unusual seasonal pattern: ${seasonalDeviationPercentage.toFixed(1)}% deviation from same period last cycle`,
|
|
236
|
-
potentialCauses: this.generatePotentialCauses("SEASONAL_ANOMALY", seasonalDeviationPercentage)
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
return anomalies;
|
|
241
|
-
}
|
|
242
|
-
calculateMedian(values) {
|
|
243
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
244
|
-
const mid = Math.floor(sorted.length / 2);
|
|
245
|
-
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
246
|
-
}
|
|
247
|
-
calculateMAD(values, median) {
|
|
248
|
-
const deviations = values.map((v) => Math.abs(v - median));
|
|
249
|
-
return this.calculateMedian(deviations);
|
|
250
|
-
}
|
|
251
|
-
calculateLinearTrend(values) {
|
|
252
|
-
const n = values.length;
|
|
253
|
-
const x = Array.from({ length: n }, (_, i) => i);
|
|
254
|
-
const sumX = x.reduce((a, b) => a + b, 0);
|
|
255
|
-
const sumY = values.reduce((a, b) => a + b, 0);
|
|
256
|
-
const sumXY = x.reduce((sum, xi, i) => sum + xi * values[i], 0);
|
|
257
|
-
const sumXX = x.reduce((sum, xi) => sum + xi * xi, 0);
|
|
258
|
-
const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
|
|
259
|
-
return slope;
|
|
260
|
-
}
|
|
261
|
-
getSensitivityThreshold() {
|
|
262
|
-
switch (this.config.sensitivity) {
|
|
263
|
-
case "HIGH":
|
|
264
|
-
return 2.5;
|
|
265
|
-
case "MEDIUM":
|
|
266
|
-
return 3.5;
|
|
267
|
-
case "LOW":
|
|
268
|
-
return 4.5;
|
|
269
|
-
default:
|
|
270
|
-
return 3.5;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
calculateSeverity(score, threshold) {
|
|
274
|
-
const ratio = score / threshold;
|
|
275
|
-
if (ratio > 3)
|
|
276
|
-
return "CRITICAL";
|
|
277
|
-
if (ratio > 2)
|
|
278
|
-
return "HIGH";
|
|
279
|
-
if (ratio > 1.5)
|
|
280
|
-
return "MEDIUM";
|
|
281
|
-
return "LOW";
|
|
282
|
-
}
|
|
283
|
-
generateAnomalyDescription(type, deviation, deviationPercentage) {
|
|
284
|
-
const direction = deviation > 0 ? "increase" : "decrease";
|
|
285
|
-
const magnitude = deviationPercentage > 100 ? "massive" : deviationPercentage > 50 ? "significant" : deviationPercentage > 25 ? "notable" : "minor";
|
|
286
|
-
return `${type.toLowerCase()} anomaly detected: ${magnitude} ${direction} of ${deviationPercentage.toFixed(1)}%`;
|
|
287
|
-
}
|
|
288
|
-
generatePotentialCauses(type, deviationPercentage) {
|
|
289
|
-
const causes = [];
|
|
290
|
-
switch (type) {
|
|
291
|
-
case "SPIKE":
|
|
292
|
-
causes.push("Increased resource usage or traffic");
|
|
293
|
-
causes.push("New service deployments or scaling events");
|
|
294
|
-
causes.push("Data transfer spikes or storage usage increases");
|
|
295
|
-
if (deviationPercentage > 50) {
|
|
296
|
-
causes.push("Potential security incident or DDoS attack");
|
|
297
|
-
causes.push("Misconfigured auto-scaling rules");
|
|
298
|
-
}
|
|
299
|
-
break;
|
|
300
|
-
case "DROP":
|
|
301
|
-
causes.push("Reduced usage or traffic patterns");
|
|
302
|
-
causes.push("Service shutdowns or downscaling");
|
|
303
|
-
causes.push("Resource optimization implementations");
|
|
304
|
-
if (deviationPercentage > 50) {
|
|
305
|
-
causes.push("Service outages or failures");
|
|
306
|
-
causes.push("Billing or account issues");
|
|
307
|
-
}
|
|
308
|
-
break;
|
|
309
|
-
case "TREND_CHANGE":
|
|
310
|
-
causes.push("Business growth or contraction");
|
|
311
|
-
causes.push("Architectural changes or migrations");
|
|
312
|
-
causes.push("New feature rollouts or service changes");
|
|
313
|
-
causes.push("Seasonal business pattern shifts");
|
|
314
|
-
break;
|
|
315
|
-
case "SEASONAL_ANOMALY":
|
|
316
|
-
causes.push("Unusual business events or promotions");
|
|
317
|
-
causes.push("Holiday pattern deviations");
|
|
318
|
-
causes.push("Market or economic factors");
|
|
319
|
-
causes.push("Competitor actions or market changes");
|
|
320
|
-
break;
|
|
321
|
-
}
|
|
322
|
-
return causes;
|
|
323
|
-
}
|
|
324
|
-
consolidateAnomalies(anomalies) {
|
|
325
|
-
const groupedAnomalies = /* @__PURE__ */ new Map();
|
|
326
|
-
anomalies.forEach((anomaly) => {
|
|
327
|
-
const key = anomaly.timestamp;
|
|
328
|
-
if (!groupedAnomalies.has(key)) {
|
|
329
|
-
groupedAnomalies.set(key, []);
|
|
330
|
-
}
|
|
331
|
-
groupedAnomalies.get(key).push(anomaly);
|
|
332
|
-
});
|
|
333
|
-
const consolidated = [];
|
|
334
|
-
groupedAnomalies.forEach((group) => {
|
|
335
|
-
const sorted = group.sort((a, b) => {
|
|
336
|
-
const severityOrder = { "CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1 };
|
|
337
|
-
if (severityOrder[a.severity] !== severityOrder[b.severity]) {
|
|
338
|
-
return severityOrder[b.severity] - severityOrder[a.severity];
|
|
339
|
-
}
|
|
340
|
-
return b.confidence - a.confidence;
|
|
341
|
-
});
|
|
342
|
-
consolidated.push(sorted[0]);
|
|
343
|
-
});
|
|
344
|
-
return consolidated.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
345
|
-
}
|
|
346
|
-
};
|
|
347
|
-
var CostAnalyticsEngine = class {
|
|
348
|
-
constructor(config) {
|
|
349
|
-
this.anomalyDetector = new AnomalyDetector(config);
|
|
350
|
-
}
|
|
351
|
-
analyzeProvider(provider, costData, serviceData) {
|
|
352
|
-
const analytics = {
|
|
353
|
-
provider,
|
|
354
|
-
analysisDate: (/* @__PURE__ */ new Date()).toISOString(),
|
|
355
|
-
overallAnomalies: this.anomalyDetector.detectAnomalies(costData),
|
|
356
|
-
serviceAnomalies: {},
|
|
357
|
-
insights: this.generateInsights(costData),
|
|
358
|
-
recommendations: this.generateRecommendations(costData)
|
|
359
|
-
};
|
|
360
|
-
if (serviceData) {
|
|
361
|
-
Object.entries(serviceData).forEach(([service, data]) => {
|
|
362
|
-
analytics.serviceAnomalies[service] = this.anomalyDetector.detectAnomalies(data);
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
return analytics;
|
|
366
|
-
}
|
|
367
|
-
generateInsights(costData) {
|
|
368
|
-
const insights = [];
|
|
369
|
-
const values = costData.map((dp) => dp.value);
|
|
370
|
-
if (values.length < 7)
|
|
371
|
-
return insights;
|
|
372
|
-
const latest = values[values.length - 1];
|
|
373
|
-
const weekAgo = values[values.length - 7];
|
|
374
|
-
const monthAgo = values.length > 30 ? values[values.length - 30] : values[0];
|
|
375
|
-
const weekGrowth = weekAgo > 0 ? (latest - weekAgo) / weekAgo * 100 : 0;
|
|
376
|
-
const monthGrowth = monthAgo > 0 ? (latest - monthAgo) / monthAgo * 100 : 0;
|
|
377
|
-
if (Math.abs(weekGrowth) > 15) {
|
|
378
|
-
insights.push(`Significant week-over-week cost ${weekGrowth > 0 ? "increase" : "decrease"} of ${Math.abs(weekGrowth).toFixed(1)}%`);
|
|
379
|
-
}
|
|
380
|
-
if (Math.abs(monthGrowth) > 25) {
|
|
381
|
-
insights.push(`Notable month-over-month cost ${monthGrowth > 0 ? "growth" : "reduction"} of ${Math.abs(monthGrowth).toFixed(1)}%`);
|
|
382
|
-
}
|
|
383
|
-
const volatility = this.calculateVolatility(values);
|
|
384
|
-
if (volatility > 0.3) {
|
|
385
|
-
insights.push(`High cost volatility detected (${(volatility * 100).toFixed(1)}%) - consider investigating irregular spending patterns`);
|
|
386
|
-
}
|
|
387
|
-
return insights;
|
|
388
|
-
}
|
|
389
|
-
generateRecommendations(costData) {
|
|
390
|
-
const recommendations = [];
|
|
391
|
-
const values = costData.map((dp) => dp.value);
|
|
392
|
-
const volatility = this.calculateVolatility(values);
|
|
393
|
-
const trend = this.anomalyDetector["calculateLinearTrend"](values.slice(-14));
|
|
394
|
-
if (volatility > 0.2) {
|
|
395
|
-
recommendations.push("Implement cost budgets and alerts to better track spending variations");
|
|
396
|
-
recommendations.push("Consider using reserved instances or savings plans for more predictable costs");
|
|
397
|
-
}
|
|
398
|
-
if (trend > 0.1) {
|
|
399
|
-
recommendations.push("Cost trend is increasing - review recent resource additions and scaling policies");
|
|
400
|
-
recommendations.push("Consider implementing automated cost optimization tools");
|
|
401
|
-
}
|
|
402
|
-
return recommendations;
|
|
403
|
-
}
|
|
404
|
-
calculateVolatility(values) {
|
|
405
|
-
if (values.length < 2)
|
|
406
|
-
return 0;
|
|
407
|
-
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
408
|
-
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
|
409
|
-
const stdDev = Math.sqrt(variance);
|
|
410
|
-
return mean > 0 ? stdDev / mean : 0;
|
|
411
|
-
}
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
// src/providers/aws.ts
|
|
415
|
-
var AWSProvider = class extends CloudProviderAdapter {
|
|
416
|
-
constructor(config) {
|
|
417
|
-
super(config);
|
|
418
|
-
}
|
|
419
|
-
getCredentials() {
|
|
420
|
-
return this.config.credentials;
|
|
421
|
-
}
|
|
422
|
-
getRegion() {
|
|
423
|
-
return this.config.region || "us-east-1";
|
|
424
|
-
}
|
|
425
|
-
async validateCredentials() {
|
|
426
|
-
try {
|
|
427
|
-
const sts = new import_client_sts.STSClient({
|
|
428
|
-
credentials: this.getCredentials(),
|
|
429
|
-
region: this.getRegion()
|
|
430
|
-
});
|
|
431
|
-
await sts.send(new import_client_sts.GetCallerIdentityCommand({}));
|
|
432
|
-
return true;
|
|
433
|
-
} catch {
|
|
434
|
-
return false;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
async getAccountInfo() {
|
|
438
|
-
showSpinner("Getting AWS account information");
|
|
439
|
-
try {
|
|
440
|
-
const iam = new import_client_iam.IAMClient({
|
|
441
|
-
credentials: this.getCredentials(),
|
|
442
|
-
region: this.getRegion()
|
|
443
|
-
});
|
|
444
|
-
const accountAliases = await iam.send(new import_client_iam.ListAccountAliasesCommand({}));
|
|
445
|
-
const foundAlias = accountAliases?.AccountAliases?.[0];
|
|
446
|
-
if (foundAlias) {
|
|
447
|
-
return {
|
|
448
|
-
id: foundAlias,
|
|
449
|
-
name: foundAlias,
|
|
450
|
-
provider: "aws" /* AWS */
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
const sts = new import_client_sts.STSClient({
|
|
454
|
-
credentials: this.getCredentials(),
|
|
455
|
-
region: this.getRegion()
|
|
456
|
-
});
|
|
457
|
-
const accountInfo = await sts.send(new import_client_sts.GetCallerIdentityCommand({}));
|
|
458
|
-
return {
|
|
459
|
-
id: accountInfo.Account || "unknown",
|
|
460
|
-
name: accountInfo.Account || "unknown",
|
|
461
|
-
provider: "aws" /* AWS */
|
|
462
|
-
};
|
|
463
|
-
} catch (error) {
|
|
464
|
-
throw new Error(`Failed to get AWS account information: ${error.message}`);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
async getRawCostData() {
|
|
468
|
-
showSpinner("Getting AWS pricing data");
|
|
469
|
-
try {
|
|
470
|
-
const costExplorer = new import_client_cost_explorer.CostExplorerClient({
|
|
471
|
-
credentials: this.getCredentials(),
|
|
472
|
-
region: this.getRegion()
|
|
473
|
-
});
|
|
474
|
-
const endDate = (0, import_dayjs.default)().subtract(1, "day");
|
|
475
|
-
const startDate = endDate.subtract(65, "day");
|
|
476
|
-
const pricingData = await costExplorer.send(new import_client_cost_explorer.GetCostAndUsageCommand({
|
|
477
|
-
TimePeriod: {
|
|
478
|
-
Start: startDate.format("YYYY-MM-DD"),
|
|
479
|
-
End: endDate.format("YYYY-MM-DD")
|
|
480
|
-
},
|
|
481
|
-
Granularity: "DAILY",
|
|
482
|
-
Filter: {
|
|
483
|
-
Not: {
|
|
484
|
-
Dimensions: {
|
|
485
|
-
Key: "RECORD_TYPE",
|
|
486
|
-
Values: ["Credit", "Refund", "Upfront", "Support"]
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
},
|
|
490
|
-
Metrics: ["UnblendedCost"],
|
|
491
|
-
GroupBy: [
|
|
492
|
-
{
|
|
493
|
-
Type: "DIMENSION",
|
|
494
|
-
Key: "SERVICE"
|
|
495
|
-
}
|
|
496
|
-
]
|
|
497
|
-
}));
|
|
498
|
-
const costByService = {};
|
|
499
|
-
for (const day of pricingData.ResultsByTime || []) {
|
|
500
|
-
for (const group of day.Groups || []) {
|
|
501
|
-
const serviceName = group.Keys?.[0];
|
|
502
|
-
const cost = group.Metrics?.UnblendedCost?.Amount;
|
|
503
|
-
const costDate = day.TimePeriod?.End;
|
|
504
|
-
if (serviceName && cost && costDate) {
|
|
505
|
-
costByService[serviceName] = costByService[serviceName] || {};
|
|
506
|
-
costByService[serviceName][costDate] = parseFloat(cost);
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
return costByService;
|
|
511
|
-
} catch (error) {
|
|
512
|
-
throw new Error(`Failed to get AWS cost data: ${error.message}`);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
async getCostBreakdown() {
|
|
516
|
-
const rawCostData = await this.getRawCostData();
|
|
517
|
-
return this.calculateServiceTotals(rawCostData);
|
|
518
|
-
}
|
|
519
|
-
async getResourceInventory(filters) {
|
|
520
|
-
showSpinner("Discovering AWS resources");
|
|
521
|
-
const regions = filters?.regions || [this.config.region || "us-east-1"];
|
|
522
|
-
const resourceTypes = filters?.resourceTypes || Object.values(ResourceType);
|
|
523
|
-
const includeCosts = filters?.includeCosts || false;
|
|
524
|
-
const inventory = {
|
|
525
|
-
provider: "aws" /* AWS */,
|
|
526
|
-
region: regions.join(", "),
|
|
527
|
-
totalResources: 0,
|
|
528
|
-
resourcesByType: {
|
|
529
|
-
["compute" /* COMPUTE */]: 0,
|
|
530
|
-
["storage" /* STORAGE */]: 0,
|
|
531
|
-
["database" /* DATABASE */]: 0,
|
|
532
|
-
["network" /* NETWORK */]: 0,
|
|
533
|
-
["security" /* SECURITY */]: 0,
|
|
534
|
-
["serverless" /* SERVERLESS */]: 0,
|
|
535
|
-
["container" /* CONTAINER */]: 0,
|
|
536
|
-
["analytics" /* ANALYTICS */]: 0
|
|
537
|
-
},
|
|
538
|
-
totalCost: 0,
|
|
539
|
-
resources: {
|
|
540
|
-
compute: [],
|
|
541
|
-
storage: [],
|
|
542
|
-
database: [],
|
|
543
|
-
network: [],
|
|
544
|
-
security: [],
|
|
545
|
-
serverless: [],
|
|
546
|
-
container: [],
|
|
547
|
-
analytics: []
|
|
548
|
-
},
|
|
549
|
-
lastUpdated: /* @__PURE__ */ new Date()
|
|
550
|
-
};
|
|
551
|
-
for (const region of regions) {
|
|
552
|
-
try {
|
|
553
|
-
if (resourceTypes.includes("compute" /* COMPUTE */)) {
|
|
554
|
-
const ec2Resources = await this.discoverEC2Instances(region, includeCosts);
|
|
555
|
-
inventory.resources.compute.push(...ec2Resources);
|
|
556
|
-
inventory.resourcesByType["compute" /* COMPUTE */] += ec2Resources.length;
|
|
557
|
-
}
|
|
558
|
-
if (resourceTypes.includes("storage" /* STORAGE */)) {
|
|
559
|
-
const s3Resources = await this.discoverS3Buckets(region, includeCosts);
|
|
560
|
-
const ebsResources = await this.discoverEBSVolumes(region, includeCosts);
|
|
561
|
-
inventory.resources.storage.push(...s3Resources, ...ebsResources);
|
|
562
|
-
inventory.resourcesByType["storage" /* STORAGE */] += s3Resources.length + ebsResources.length;
|
|
563
|
-
}
|
|
564
|
-
if (resourceTypes.includes("database" /* DATABASE */)) {
|
|
565
|
-
const rdsResources = await this.discoverRDSInstances(region, includeCosts);
|
|
566
|
-
inventory.resources.database.push(...rdsResources);
|
|
567
|
-
inventory.resourcesByType["database" /* DATABASE */] += rdsResources.length;
|
|
568
|
-
}
|
|
569
|
-
if (resourceTypes.includes("serverless" /* SERVERLESS */)) {
|
|
570
|
-
const lambdaResources = await this.discoverLambdaFunctions(region, includeCosts);
|
|
571
|
-
inventory.resources.serverless.push(...lambdaResources);
|
|
572
|
-
inventory.resourcesByType["serverless" /* SERVERLESS */] += lambdaResources.length;
|
|
573
|
-
}
|
|
574
|
-
if (resourceTypes.includes("network" /* NETWORK */)) {
|
|
575
|
-
const vpcResources = await this.discoverVPCs(region, includeCosts);
|
|
576
|
-
const subnetResources = await this.discoverSubnets(region, includeCosts);
|
|
577
|
-
inventory.resources.network.push(...vpcResources, ...subnetResources);
|
|
578
|
-
inventory.resourcesByType["network" /* NETWORK */] += vpcResources.length + subnetResources.length;
|
|
579
|
-
}
|
|
580
|
-
} catch (error) {
|
|
581
|
-
console.warn(`Failed to discover resources in region ${region}: ${error.message}`);
|
|
582
|
-
}
|
|
583
|
-
}
|
|
584
|
-
inventory.totalResources = Object.values(inventory.resourcesByType).reduce((sum, count) => sum + count, 0);
|
|
585
|
-
if (includeCosts) {
|
|
586
|
-
inventory.totalCost = Object.values(inventory.resources).flat().reduce((sum, resource) => sum + (resource.costToDate || 0), 0);
|
|
587
|
-
}
|
|
588
|
-
return inventory;
|
|
589
|
-
}
|
|
590
|
-
async discoverEC2Instances(region, includeCosts) {
|
|
591
|
-
try {
|
|
592
|
-
const ec2Client = new import_client_ec2.EC2Client({
|
|
593
|
-
credentials: this.getCredentials(),
|
|
594
|
-
region
|
|
595
|
-
});
|
|
596
|
-
const command = new import_client_ec2.DescribeInstancesCommand({});
|
|
597
|
-
const result = await ec2Client.send(command);
|
|
598
|
-
const instances = [];
|
|
599
|
-
for (const reservation of result.Reservations || []) {
|
|
600
|
-
for (const instance of reservation.Instances || []) {
|
|
601
|
-
const ec2Instance = {
|
|
602
|
-
id: instance.InstanceId || "",
|
|
603
|
-
name: instance.Tags?.find((tag) => tag.Key === "Name")?.Value || instance.InstanceId || "",
|
|
604
|
-
state: instance.State?.Name || "unknown",
|
|
605
|
-
region,
|
|
606
|
-
tags: instance.Tags?.reduce((acc, tag) => {
|
|
607
|
-
if (tag.Key && tag.Value)
|
|
608
|
-
acc[tag.Key] = tag.Value;
|
|
609
|
-
return acc;
|
|
610
|
-
}, {}),
|
|
611
|
-
createdAt: instance.LaunchTime || /* @__PURE__ */ new Date(),
|
|
612
|
-
provider: "aws" /* AWS */,
|
|
613
|
-
instanceType: instance.InstanceType,
|
|
614
|
-
cpu: this.getCpuCountForInstanceType(instance.InstanceType),
|
|
615
|
-
memory: this.getMemoryForInstanceType(instance.InstanceType),
|
|
616
|
-
platform: instance.Platform || "linux",
|
|
617
|
-
instanceId: instance.InstanceId || "",
|
|
618
|
-
imageId: instance.ImageId || "",
|
|
619
|
-
keyName: instance.KeyName,
|
|
620
|
-
securityGroups: instance.SecurityGroups?.map((sg) => sg.GroupId || "") || [],
|
|
621
|
-
subnetId: instance.SubnetId || "",
|
|
622
|
-
vpcId: instance.VpcId || "",
|
|
623
|
-
publicDnsName: instance.PublicDnsName,
|
|
624
|
-
privateDnsName: instance.PrivateDnsName,
|
|
625
|
-
monitoring: instance.Monitoring?.State === "enabled",
|
|
626
|
-
placement: {
|
|
627
|
-
availabilityZone: instance.Placement?.AvailabilityZone || "",
|
|
628
|
-
groupName: instance.Placement?.GroupName
|
|
629
|
-
},
|
|
630
|
-
publicIp: instance.PublicIpAddress,
|
|
631
|
-
privateIp: instance.PrivateIpAddress
|
|
632
|
-
};
|
|
633
|
-
if (includeCosts) {
|
|
634
|
-
ec2Instance.costToDate = await this.getResourceCosts(instance.InstanceId || "");
|
|
635
|
-
}
|
|
636
|
-
instances.push(ec2Instance);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return instances;
|
|
640
|
-
} catch (error) {
|
|
641
|
-
console.warn(`Failed to discover EC2 instances in ${region}: ${error.message}`);
|
|
642
|
-
return [];
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
async discoverS3Buckets(region, includeCosts) {
|
|
646
|
-
try {
|
|
647
|
-
const s3Client = new import_client_s3.S3Client({
|
|
648
|
-
credentials: this.getCredentials(),
|
|
649
|
-
region
|
|
650
|
-
});
|
|
651
|
-
const command = new import_client_s3.ListBucketsCommand({});
|
|
652
|
-
const result = await s3Client.send(command);
|
|
653
|
-
const buckets = [];
|
|
654
|
-
for (const bucket of result.Buckets || []) {
|
|
655
|
-
if (!bucket.Name)
|
|
656
|
-
continue;
|
|
657
|
-
const s3Bucket = {
|
|
658
|
-
id: bucket.Name,
|
|
659
|
-
name: bucket.Name,
|
|
660
|
-
state: "active",
|
|
661
|
-
region,
|
|
662
|
-
createdAt: bucket.CreationDate || /* @__PURE__ */ new Date(),
|
|
663
|
-
provider: "aws" /* AWS */,
|
|
664
|
-
sizeGB: 0,
|
|
665
|
-
// Would need additional API calls to get actual size
|
|
666
|
-
storageType: "S3",
|
|
667
|
-
bucketName: bucket.Name
|
|
668
|
-
};
|
|
669
|
-
if (includeCosts) {
|
|
670
|
-
s3Bucket.costToDate = await this.getResourceCosts(bucket.Name);
|
|
671
|
-
}
|
|
672
|
-
buckets.push(s3Bucket);
|
|
673
|
-
}
|
|
674
|
-
return buckets;
|
|
675
|
-
} catch (error) {
|
|
676
|
-
console.warn(`Failed to discover S3 buckets: ${error.message}`);
|
|
677
|
-
return [];
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
async discoverEBSVolumes(region, includeCosts) {
|
|
681
|
-
try {
|
|
682
|
-
const ec2Client = new import_client_ec2.EC2Client({
|
|
683
|
-
credentials: this.getCredentials(),
|
|
684
|
-
region
|
|
685
|
-
});
|
|
686
|
-
const command = new import_client_ec2.DescribeVolumesCommand({});
|
|
687
|
-
const result = await ec2Client.send(command);
|
|
688
|
-
const volumes = [];
|
|
689
|
-
for (const volume of result.Volumes || []) {
|
|
690
|
-
if (!volume.VolumeId)
|
|
691
|
-
continue;
|
|
692
|
-
const ebsVolume = {
|
|
693
|
-
id: volume.VolumeId,
|
|
694
|
-
name: volume.Tags?.find((tag) => tag.Key === "Name")?.Value || volume.VolumeId,
|
|
695
|
-
state: volume.State || "unknown",
|
|
696
|
-
region,
|
|
697
|
-
tags: volume.Tags?.reduce((acc, tag) => {
|
|
698
|
-
if (tag.Key && tag.Value)
|
|
699
|
-
acc[tag.Key] = tag.Value;
|
|
700
|
-
return acc;
|
|
701
|
-
}, {}),
|
|
702
|
-
createdAt: volume.CreateTime || /* @__PURE__ */ new Date(),
|
|
703
|
-
provider: "aws" /* AWS */,
|
|
704
|
-
sizeGB: volume.Size || 0,
|
|
705
|
-
storageType: volume.VolumeType || "gp2",
|
|
706
|
-
encrypted: volume.Encrypted,
|
|
707
|
-
volumeId: volume.VolumeId,
|
|
708
|
-
volumeType: volume.VolumeType || "gp2",
|
|
709
|
-
iops: volume.Iops,
|
|
710
|
-
throughput: volume.Throughput,
|
|
711
|
-
attachments: volume.Attachments?.map((attachment) => ({
|
|
712
|
-
instanceId: attachment.InstanceId || "",
|
|
713
|
-
device: attachment.Device || ""
|
|
714
|
-
})),
|
|
715
|
-
snapshotId: volume.SnapshotId
|
|
716
|
-
};
|
|
717
|
-
if (includeCosts) {
|
|
718
|
-
ebsVolume.costToDate = await this.getResourceCosts(volume.VolumeId);
|
|
719
|
-
}
|
|
720
|
-
volumes.push(ebsVolume);
|
|
721
|
-
}
|
|
722
|
-
return volumes;
|
|
723
|
-
} catch (error) {
|
|
724
|
-
console.warn(`Failed to discover EBS volumes in ${region}: ${error.message}`);
|
|
725
|
-
return [];
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
async discoverRDSInstances(region, includeCosts) {
|
|
729
|
-
try {
|
|
730
|
-
const rdsClient = new import_client_rds.RDSClient({
|
|
731
|
-
credentials: this.getCredentials(),
|
|
732
|
-
region
|
|
733
|
-
});
|
|
734
|
-
const command = new import_client_rds.DescribeDBInstancesCommand({});
|
|
735
|
-
const result = await rdsClient.send(command);
|
|
736
|
-
const instances = [];
|
|
737
|
-
for (const dbInstance of result.DBInstances || []) {
|
|
738
|
-
if (!dbInstance.DBInstanceIdentifier)
|
|
739
|
-
continue;
|
|
740
|
-
const rdsInstance = {
|
|
741
|
-
id: dbInstance.DBInstanceIdentifier,
|
|
742
|
-
name: dbInstance.DBName || dbInstance.DBInstanceIdentifier,
|
|
743
|
-
state: dbInstance.DBInstanceStatus || "unknown",
|
|
744
|
-
region,
|
|
745
|
-
createdAt: dbInstance.InstanceCreateTime || /* @__PURE__ */ new Date(),
|
|
746
|
-
provider: "aws" /* AWS */,
|
|
747
|
-
engine: dbInstance.Engine || "",
|
|
748
|
-
version: dbInstance.EngineVersion || "",
|
|
749
|
-
instanceClass: dbInstance.DBInstanceClass,
|
|
750
|
-
storageGB: dbInstance.AllocatedStorage,
|
|
751
|
-
multiAZ: dbInstance.MultiAZ,
|
|
752
|
-
dbInstanceIdentifier: dbInstance.DBInstanceIdentifier,
|
|
753
|
-
dbName: dbInstance.DBName,
|
|
754
|
-
masterUsername: dbInstance.MasterUsername || "",
|
|
755
|
-
endpoint: dbInstance.Endpoint?.Address,
|
|
756
|
-
port: dbInstance.Endpoint?.Port,
|
|
757
|
-
availabilityZone: dbInstance.AvailabilityZone,
|
|
758
|
-
backupRetentionPeriod: dbInstance.BackupRetentionPeriod,
|
|
759
|
-
storageEncrypted: dbInstance.StorageEncrypted
|
|
760
|
-
};
|
|
761
|
-
if (includeCosts) {
|
|
762
|
-
rdsInstance.costToDate = await this.getResourceCosts(dbInstance.DBInstanceIdentifier);
|
|
763
|
-
}
|
|
764
|
-
instances.push(rdsInstance);
|
|
765
|
-
}
|
|
766
|
-
return instances;
|
|
767
|
-
} catch (error) {
|
|
768
|
-
console.warn(`Failed to discover RDS instances in ${region}: ${error.message}`);
|
|
769
|
-
return [];
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
async discoverLambdaFunctions(region, includeCosts) {
|
|
773
|
-
try {
|
|
774
|
-
const lambdaClient = new import_client_lambda.LambdaClient({
|
|
775
|
-
credentials: this.getCredentials(),
|
|
776
|
-
region
|
|
777
|
-
});
|
|
778
|
-
const command = new import_client_lambda.ListFunctionsCommand({});
|
|
779
|
-
const result = await lambdaClient.send(command);
|
|
780
|
-
const functions = [];
|
|
781
|
-
for (const func of result.Functions || []) {
|
|
782
|
-
if (!func.FunctionName)
|
|
783
|
-
continue;
|
|
784
|
-
const lambdaFunction = {
|
|
785
|
-
id: func.FunctionArn || func.FunctionName,
|
|
786
|
-
name: func.FunctionName,
|
|
787
|
-
state: func.State || "unknown",
|
|
788
|
-
region,
|
|
789
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
790
|
-
// Lambda doesn't provide creation time in list
|
|
791
|
-
provider: "aws" /* AWS */,
|
|
792
|
-
functionName: func.FunctionName,
|
|
793
|
-
runtime: func.Runtime || "",
|
|
794
|
-
handler: func.Handler || "",
|
|
795
|
-
codeSize: func.CodeSize || 0,
|
|
796
|
-
timeout: func.Timeout || 0,
|
|
797
|
-
memorySize: func.MemorySize || 0,
|
|
798
|
-
lastModified: new Date(func.LastModified || ""),
|
|
799
|
-
version: func.Version || ""
|
|
800
|
-
};
|
|
801
|
-
if (includeCosts) {
|
|
802
|
-
lambdaFunction.costToDate = await this.getResourceCosts(func.FunctionName);
|
|
803
|
-
}
|
|
804
|
-
functions.push(lambdaFunction);
|
|
805
|
-
}
|
|
806
|
-
return functions;
|
|
807
|
-
} catch (error) {
|
|
808
|
-
console.warn(`Failed to discover Lambda functions in ${region}: ${error.message}`);
|
|
809
|
-
return [];
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
async discoverVPCs(region, includeCosts) {
|
|
813
|
-
try {
|
|
814
|
-
const ec2Client = new import_client_ec2.EC2Client({
|
|
815
|
-
credentials: this.getCredentials(),
|
|
816
|
-
region
|
|
817
|
-
});
|
|
818
|
-
const command = new import_client_ec2.DescribeVpcsCommand({});
|
|
819
|
-
const result = await ec2Client.send(command);
|
|
820
|
-
const vpcs = [];
|
|
821
|
-
for (const vpc of result.Vpcs || []) {
|
|
822
|
-
if (!vpc.VpcId)
|
|
823
|
-
continue;
|
|
824
|
-
const awsVpc = {
|
|
825
|
-
id: vpc.VpcId,
|
|
826
|
-
name: vpc.Tags?.find((tag) => tag.Key === "Name")?.Value || vpc.VpcId,
|
|
827
|
-
state: vpc.State || "unknown",
|
|
828
|
-
region,
|
|
829
|
-
tags: vpc.Tags?.reduce((acc, tag) => {
|
|
830
|
-
if (tag.Key && tag.Value)
|
|
831
|
-
acc[tag.Key] = tag.Value;
|
|
832
|
-
return acc;
|
|
833
|
-
}, {}),
|
|
834
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
835
|
-
// VPC doesn't provide creation time
|
|
836
|
-
provider: "aws" /* AWS */,
|
|
837
|
-
vpcId: vpc.VpcId,
|
|
838
|
-
cidrBlock: vpc.CidrBlock || "",
|
|
839
|
-
dhcpOptionsId: vpc.DhcpOptionsId || "",
|
|
840
|
-
isDefault: vpc.IsDefault || false
|
|
841
|
-
};
|
|
842
|
-
if (includeCosts) {
|
|
843
|
-
awsVpc.costToDate = await this.getResourceCosts(vpc.VpcId);
|
|
844
|
-
}
|
|
845
|
-
vpcs.push(awsVpc);
|
|
846
|
-
}
|
|
847
|
-
return vpcs;
|
|
848
|
-
} catch (error) {
|
|
849
|
-
console.warn(`Failed to discover VPCs in ${region}: ${error.message}`);
|
|
850
|
-
return [];
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
async discoverSubnets(region, includeCosts) {
|
|
854
|
-
try {
|
|
855
|
-
const ec2Client = new import_client_ec2.EC2Client({
|
|
856
|
-
credentials: this.getCredentials(),
|
|
857
|
-
region
|
|
858
|
-
});
|
|
859
|
-
const command = new import_client_ec2.DescribeSubnetsCommand({});
|
|
860
|
-
const result = await ec2Client.send(command);
|
|
861
|
-
const subnets = [];
|
|
862
|
-
for (const subnet of result.Subnets || []) {
|
|
863
|
-
if (!subnet.SubnetId)
|
|
864
|
-
continue;
|
|
865
|
-
const awsSubnet = {
|
|
866
|
-
id: subnet.SubnetId,
|
|
867
|
-
name: subnet.Tags?.find((tag) => tag.Key === "Name")?.Value || subnet.SubnetId,
|
|
868
|
-
state: subnet.State || "unknown",
|
|
869
|
-
region,
|
|
870
|
-
tags: subnet.Tags?.reduce((acc, tag) => {
|
|
871
|
-
if (tag.Key && tag.Value)
|
|
872
|
-
acc[tag.Key] = tag.Value;
|
|
873
|
-
return acc;
|
|
874
|
-
}, {}),
|
|
875
|
-
createdAt: /* @__PURE__ */ new Date(),
|
|
876
|
-
// Subnet doesn't provide creation time
|
|
877
|
-
provider: "aws" /* AWS */,
|
|
878
|
-
subnetId: subnet.SubnetId,
|
|
879
|
-
vpcId: subnet.VpcId || "",
|
|
880
|
-
cidrBlock: subnet.CidrBlock || "",
|
|
881
|
-
availableIpAddressCount: subnet.AvailableIpAddressCount || 0,
|
|
882
|
-
availabilityZone: subnet.AvailabilityZone || "",
|
|
883
|
-
mapPublicIpOnLaunch: subnet.MapPublicIpOnLaunch || false
|
|
884
|
-
};
|
|
885
|
-
if (includeCosts) {
|
|
886
|
-
awsSubnet.costToDate = await this.getResourceCosts(subnet.SubnetId);
|
|
887
|
-
}
|
|
888
|
-
subnets.push(awsSubnet);
|
|
889
|
-
}
|
|
890
|
-
return subnets;
|
|
891
|
-
} catch (error) {
|
|
892
|
-
console.warn(`Failed to discover Subnets in ${region}: ${error.message}`);
|
|
893
|
-
return [];
|
|
894
|
-
}
|
|
895
|
-
}
|
|
896
|
-
async getResourceCosts(resourceId) {
|
|
897
|
-
return 0;
|
|
898
|
-
}
|
|
899
|
-
async getOptimizationRecommendations() {
|
|
900
|
-
const recommendations = [];
|
|
901
|
-
try {
|
|
902
|
-
recommendations.push(
|
|
903
|
-
"Consider using Reserved Instances for long-running EC2 instances to save up to 72%",
|
|
904
|
-
"Enable S3 Intelligent Tiering for automatic cost optimization",
|
|
905
|
-
"Review underutilized RDS instances and consider right-sizing",
|
|
906
|
-
"Implement lifecycle policies for EBS snapshots older than 30 days",
|
|
907
|
-
"Consider using Spot Instances for fault-tolerant workloads"
|
|
908
|
-
);
|
|
909
|
-
} catch (error) {
|
|
910
|
-
console.warn(`Failed to generate optimization recommendations: ${error.message}`);
|
|
911
|
-
}
|
|
912
|
-
return recommendations;
|
|
913
|
-
}
|
|
914
|
-
async getBudgets() {
|
|
915
|
-
showSpinner("Getting AWS budgets");
|
|
916
|
-
try {
|
|
917
|
-
const budgetsClient = new import_client_budgets.BudgetsClient({
|
|
918
|
-
credentials: this.getCredentials(),
|
|
919
|
-
region: this.getRegion()
|
|
920
|
-
});
|
|
921
|
-
const sts = new import_client_sts.STSClient({
|
|
922
|
-
credentials: this.getCredentials(),
|
|
923
|
-
region: this.getRegion()
|
|
924
|
-
});
|
|
925
|
-
const accountInfo = await sts.send(new import_client_sts.GetCallerIdentityCommand({}));
|
|
926
|
-
const accountId = accountInfo.Account;
|
|
927
|
-
if (!accountId) {
|
|
928
|
-
throw new Error("Unable to determine AWS account ID");
|
|
929
|
-
}
|
|
930
|
-
const budgetsResponse = await budgetsClient.send(new import_client_budgets.DescribeBudgetsCommand({
|
|
931
|
-
AccountId: accountId
|
|
932
|
-
}));
|
|
933
|
-
const budgets = [];
|
|
934
|
-
for (const budget of budgetsResponse.Budgets || []) {
|
|
935
|
-
if (!budget.BudgetName || !budget.BudgetLimit)
|
|
936
|
-
continue;
|
|
937
|
-
const budgetInfo = {
|
|
938
|
-
budgetName: budget.BudgetName,
|
|
939
|
-
budgetLimit: parseFloat(budget.BudgetLimit.Amount || "0"),
|
|
940
|
-
actualSpend: parseFloat(budget.CalculatedSpend?.ActualSpend?.Amount || "0"),
|
|
941
|
-
forecastedSpend: parseFloat(budget.CalculatedSpend?.ForecastedSpend?.Amount || "0"),
|
|
942
|
-
timeUnit: budget.TimeUnit || "MONTHLY",
|
|
943
|
-
timePeriod: {
|
|
944
|
-
start: typeof budget.TimePeriod?.Start === "string" ? budget.TimePeriod?.Start : budget.TimePeriod?.Start instanceof Date ? budget.TimePeriod?.Start.toISOString() : "",
|
|
945
|
-
end: typeof budget.TimePeriod?.End === "string" ? budget.TimePeriod?.End : budget.TimePeriod?.End instanceof Date ? budget.TimePeriod?.End.toISOString() : ""
|
|
946
|
-
},
|
|
947
|
-
budgetType: budget.BudgetType || "COST",
|
|
948
|
-
status: this.determineBudgetStatus(budget),
|
|
949
|
-
thresholds: this.parseBudgetThresholds(budget),
|
|
950
|
-
costFilters: this.parseCostFilters(budget)
|
|
951
|
-
};
|
|
952
|
-
budgets.push(budgetInfo);
|
|
953
|
-
}
|
|
954
|
-
return budgets;
|
|
955
|
-
} catch (error) {
|
|
956
|
-
console.warn(`Failed to get AWS budgets: ${error.message}`);
|
|
957
|
-
return [];
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
async getBudgetAlerts() {
|
|
961
|
-
const budgets = await this.getBudgets();
|
|
962
|
-
const alerts = [];
|
|
963
|
-
for (const budget of budgets) {
|
|
964
|
-
const percentageUsed = budget.actualSpend / budget.budgetLimit * 100;
|
|
965
|
-
for (const threshold of budget.thresholds) {
|
|
966
|
-
let isExceeded = false;
|
|
967
|
-
let currentValue = 0;
|
|
968
|
-
if (threshold.thresholdType === "PERCENTAGE") {
|
|
969
|
-
currentValue = percentageUsed;
|
|
970
|
-
if (threshold.notificationType === "ACTUAL") {
|
|
971
|
-
isExceeded = percentageUsed >= threshold.threshold;
|
|
972
|
-
} else if (threshold.notificationType === "FORECASTED" && budget.forecastedSpend) {
|
|
973
|
-
const forecastedPercentage = budget.forecastedSpend / budget.budgetLimit * 100;
|
|
974
|
-
isExceeded = forecastedPercentage >= threshold.threshold;
|
|
975
|
-
currentValue = forecastedPercentage;
|
|
976
|
-
}
|
|
977
|
-
} else {
|
|
978
|
-
currentValue = budget.actualSpend;
|
|
979
|
-
if (threshold.notificationType === "ACTUAL") {
|
|
980
|
-
isExceeded = budget.actualSpend >= threshold.threshold;
|
|
981
|
-
} else if (threshold.notificationType === "FORECASTED" && budget.forecastedSpend) {
|
|
982
|
-
isExceeded = budget.forecastedSpend >= threshold.threshold;
|
|
983
|
-
currentValue = budget.forecastedSpend;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
if (isExceeded) {
|
|
987
|
-
const alert = {
|
|
988
|
-
budgetName: budget.budgetName,
|
|
989
|
-
alertType: threshold.notificationType === "FORECASTED" ? "FORECAST_EXCEEDED" : "THRESHOLD_EXCEEDED",
|
|
990
|
-
currentSpend: budget.actualSpend,
|
|
991
|
-
budgetLimit: budget.budgetLimit,
|
|
992
|
-
threshold: threshold.threshold,
|
|
993
|
-
percentageUsed,
|
|
994
|
-
timeRemaining: this.calculateTimeRemaining(budget.timePeriod),
|
|
995
|
-
severity: this.determineSeverity(percentageUsed),
|
|
996
|
-
message: this.generateAlertMessage(budget, threshold, percentageUsed)
|
|
997
|
-
};
|
|
998
|
-
alerts.push(alert);
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
return alerts;
|
|
1003
|
-
}
|
|
1004
|
-
async getCostTrendAnalysis(months = 6) {
|
|
1005
|
-
showSpinner("Analyzing cost trends");
|
|
1006
|
-
try {
|
|
1007
|
-
const costExplorer = new import_client_cost_explorer.CostExplorerClient({
|
|
1008
|
-
credentials: this.getCredentials(),
|
|
1009
|
-
region: this.getRegion()
|
|
1010
|
-
});
|
|
1011
|
-
const endDate = (0, import_dayjs.default)();
|
|
1012
|
-
const startDate = endDate.subtract(months, "month");
|
|
1013
|
-
const monthlyData = await costExplorer.send(new import_client_cost_explorer.GetCostAndUsageCommand({
|
|
1014
|
-
TimePeriod: {
|
|
1015
|
-
Start: startDate.format("YYYY-MM-DD"),
|
|
1016
|
-
End: endDate.format("YYYY-MM-DD")
|
|
1017
|
-
},
|
|
1018
|
-
Granularity: "MONTHLY",
|
|
1019
|
-
Metrics: ["UnblendedCost"],
|
|
1020
|
-
GroupBy: [
|
|
1021
|
-
{
|
|
1022
|
-
Type: "DIMENSION",
|
|
1023
|
-
Key: "SERVICE"
|
|
1024
|
-
}
|
|
1025
|
-
]
|
|
1026
|
-
}));
|
|
1027
|
-
const monthlyCosts = [];
|
|
1028
|
-
const monthlyBreakdown = [];
|
|
1029
|
-
const serviceTrends = {};
|
|
1030
|
-
let totalCost = 0;
|
|
1031
|
-
for (const result of monthlyData.ResultsByTime || []) {
|
|
1032
|
-
const period = result.TimePeriod?.Start || "";
|
|
1033
|
-
const monthCost = result.Total?.UnblendedCost?.Amount ? parseFloat(result.Total.UnblendedCost.Amount) : 0;
|
|
1034
|
-
monthlyCosts.push(monthCost);
|
|
1035
|
-
totalCost += monthCost;
|
|
1036
|
-
const services = {};
|
|
1037
|
-
result.Groups?.forEach((group) => {
|
|
1038
|
-
const serviceName = group.Keys?.[0] || "Unknown";
|
|
1039
|
-
const cost = parseFloat(group.Metrics?.UnblendedCost?.Amount || "0");
|
|
1040
|
-
services[serviceName] = cost;
|
|
1041
|
-
if (!serviceTrends[serviceName]) {
|
|
1042
|
-
serviceTrends[serviceName] = [];
|
|
1043
|
-
}
|
|
1044
|
-
serviceTrends[serviceName].push(cost);
|
|
1045
|
-
});
|
|
1046
|
-
monthlyBreakdown.push({
|
|
1047
|
-
month: period,
|
|
1048
|
-
cost: monthCost,
|
|
1049
|
-
services
|
|
1050
|
-
});
|
|
1051
|
-
}
|
|
1052
|
-
const averageDailyCost = totalCost / (months * 30);
|
|
1053
|
-
const projectedMonthlyCost = monthlyCosts.length > 0 ? monthlyCosts[monthlyCosts.length - 1] : averageDailyCost * 30;
|
|
1054
|
-
let avgMonthOverMonthGrowth = 0;
|
|
1055
|
-
if (monthlyCosts.length > 1) {
|
|
1056
|
-
const growthRates = [];
|
|
1057
|
-
for (let i = 1; i < monthlyCosts.length; i++) {
|
|
1058
|
-
const growth = (monthlyCosts[i] - monthlyCosts[i - 1]) / monthlyCosts[i - 1] * 100;
|
|
1059
|
-
growthRates.push(growth);
|
|
1060
|
-
}
|
|
1061
|
-
avgMonthOverMonthGrowth = growthRates.reduce((a, b) => a + b, 0) / growthRates.length;
|
|
1062
|
-
}
|
|
1063
|
-
const costAnomalies = [];
|
|
1064
|
-
for (let i = 0; i < monthlyCosts.length; i++) {
|
|
1065
|
-
const monthCost = monthlyCosts[i];
|
|
1066
|
-
const period = monthlyBreakdown[i]?.month || "";
|
|
1067
|
-
if (i > 0) {
|
|
1068
|
-
const prevMonthCost = monthlyCosts[i - 1];
|
|
1069
|
-
const monthOverMonthChange = (monthCost - prevMonthCost) / prevMonthCost * 100;
|
|
1070
|
-
if (Math.abs(monthOverMonthChange) > 25) {
|
|
1071
|
-
costAnomalies.push({
|
|
1072
|
-
date: period,
|
|
1073
|
-
deviation: Math.abs(monthOverMonthChange),
|
|
1074
|
-
severity: Math.abs(monthOverMonthChange) > 50 ? "CRITICAL" : "HIGH",
|
|
1075
|
-
description: `${monthOverMonthChange > 0 ? "Spike" : "Drop"} in monthly costs: ${monthOverMonthChange.toFixed(1)}% MoM change`
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
}
|
|
1079
|
-
if (i > 2) {
|
|
1080
|
-
const avgOfPrevious = monthlyCosts.slice(0, i).reduce((a, b) => a + b, 0) / i;
|
|
1081
|
-
const deviation = (monthCost - avgOfPrevious) / avgOfPrevious * 100;
|
|
1082
|
-
if (Math.abs(deviation) > 30) {
|
|
1083
|
-
costAnomalies.push({
|
|
1084
|
-
date: period,
|
|
1085
|
-
deviation: Math.abs(deviation),
|
|
1086
|
-
severity: Math.abs(deviation) > 60 ? "CRITICAL" : "MEDIUM",
|
|
1087
|
-
description: `Cost ${deviation > 0 ? "above" : "below"} ${months}-month average by ${Math.abs(deviation).toFixed(1)}%`
|
|
1088
|
-
});
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
}
|
|
1092
|
-
const serviceAnalysis = {};
|
|
1093
|
-
Object.entries(serviceTrends).forEach(([service, costs]) => {
|
|
1094
|
-
if (costs.length > 1) {
|
|
1095
|
-
const currentCost = costs[costs.length - 1];
|
|
1096
|
-
const previousCost = costs[costs.length - 2];
|
|
1097
|
-
const growthRate = (currentCost - previousCost) / previousCost * 100;
|
|
1098
|
-
const trend = Math.abs(growthRate) < 5 ? "stable" : growthRate > 0 ? "increasing" : "decreasing";
|
|
1099
|
-
serviceAnalysis[service] = {
|
|
1100
|
-
currentCost,
|
|
1101
|
-
growthRate,
|
|
1102
|
-
trend
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
});
|
|
1106
|
-
const analyticsEngine = new CostAnalyticsEngine({
|
|
1107
|
-
sensitivity: "MEDIUM",
|
|
1108
|
-
lookbackPeriods: Math.min(14, monthlyBreakdown.length),
|
|
1109
|
-
seasonalityPeriods: 12
|
|
1110
|
-
// Monthly seasonality
|
|
1111
|
-
});
|
|
1112
|
-
const dataPoints = monthlyBreakdown.map((mb) => ({
|
|
1113
|
-
timestamp: mb.month,
|
|
1114
|
-
value: mb.cost
|
|
1115
|
-
}));
|
|
1116
|
-
const analytics = analyticsEngine.analyzeProvider("aws" /* AWS */, dataPoints);
|
|
1117
|
-
const enhancedAnomalies = [...costAnomalies];
|
|
1118
|
-
analytics.overallAnomalies.forEach((anomaly) => {
|
|
1119
|
-
enhancedAnomalies.push({
|
|
1120
|
-
date: anomaly.timestamp,
|
|
1121
|
-
deviation: anomaly.deviationPercentage,
|
|
1122
|
-
severity: anomaly.severity,
|
|
1123
|
-
description: `${anomaly.description} (${anomaly.confidence.toFixed(0)}% confidence)`
|
|
1124
|
-
});
|
|
1125
|
-
});
|
|
1126
|
-
return {
|
|
1127
|
-
totalCost,
|
|
1128
|
-
averageDailyCost,
|
|
1129
|
-
projectedMonthlyCost,
|
|
1130
|
-
avgMonthOverMonthGrowth,
|
|
1131
|
-
costAnomalies: enhancedAnomalies,
|
|
1132
|
-
monthlyBreakdown,
|
|
1133
|
-
serviceTrends: serviceAnalysis,
|
|
1134
|
-
forecastAccuracy: monthlyCosts.length > 3 ? this.calculateForecastAccuracy(monthlyCosts) : 0,
|
|
1135
|
-
analytics: {
|
|
1136
|
-
insights: analytics.insights,
|
|
1137
|
-
recommendations: analytics.recommendations,
|
|
1138
|
-
volatilityScore: this.calculateVolatility(monthlyCosts),
|
|
1139
|
-
trendStrength: this.calculateTrendStrength(monthlyCosts)
|
|
1140
|
-
}
|
|
1141
|
-
};
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
console.error("Failed to get AWS cost trend analysis:", error);
|
|
1144
|
-
return this.getMockTrendAnalysis(months);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
calculateForecastAccuracy(monthlyCosts) {
|
|
1148
|
-
if (monthlyCosts.length < 4)
|
|
1149
|
-
return 0;
|
|
1150
|
-
const n = monthlyCosts.length;
|
|
1151
|
-
const lastThreeActual = monthlyCosts.slice(-3);
|
|
1152
|
-
const trainData = monthlyCosts.slice(0, -3);
|
|
1153
|
-
const prediction = trainData.reduce((a, b) => a + b, 0) / trainData.length;
|
|
1154
|
-
const actualAvg = lastThreeActual.reduce((a, b) => a + b, 0) / lastThreeActual.length;
|
|
1155
|
-
const accuracy = Math.max(0, 100 - Math.abs(prediction - actualAvg) / actualAvg * 100);
|
|
1156
|
-
return Math.round(accuracy);
|
|
1157
|
-
}
|
|
1158
|
-
getMockTrendAnalysis(months) {
|
|
1159
|
-
const monthlyCosts = Array.from({ length: months }, (_, i) => {
|
|
1160
|
-
const baseCost = 1500 + Math.random() * 500;
|
|
1161
|
-
const trend = 1 + i * 0.02;
|
|
1162
|
-
const volatility = 0.8 + Math.random() * 0.4;
|
|
1163
|
-
return baseCost * trend * volatility;
|
|
1164
|
-
});
|
|
1165
|
-
const totalCost = monthlyCosts.reduce((sum, cost) => sum + cost, 0);
|
|
1166
|
-
const averageDailyCost = totalCost / (months * 30);
|
|
1167
|
-
const projectedMonthlyCost = monthlyCosts[monthlyCosts.length - 1];
|
|
1168
|
-
let avgMonthOverMonthGrowth = 0;
|
|
1169
|
-
if (monthlyCosts.length > 1) {
|
|
1170
|
-
const growthRates = [];
|
|
1171
|
-
for (let i = 1; i < monthlyCosts.length; i++) {
|
|
1172
|
-
const growth = (monthlyCosts[i] - monthlyCosts[i - 1]) / monthlyCosts[i - 1] * 100;
|
|
1173
|
-
growthRates.push(growth);
|
|
1174
|
-
}
|
|
1175
|
-
avgMonthOverMonthGrowth = growthRates.reduce((a, b) => a + b, 0) / growthRates.length;
|
|
1176
|
-
}
|
|
1177
|
-
const costAnomalies = [];
|
|
1178
|
-
if (months >= 3) {
|
|
1179
|
-
costAnomalies.push({
|
|
1180
|
-
date: new Date(Date.now() - 60 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0],
|
|
1181
|
-
deviation: 35.2,
|
|
1182
|
-
severity: "MEDIUM",
|
|
1183
|
-
description: "Cost spike due to increased EC2 usage during Black Friday traffic"
|
|
1184
|
-
});
|
|
1185
|
-
costAnomalies.push({
|
|
1186
|
-
date: new Date(Date.now() - 90 * 24 * 60 * 60 * 1e3).toISOString().split("T")[0],
|
|
1187
|
-
deviation: 28.7,
|
|
1188
|
-
severity: "HIGH",
|
|
1189
|
-
description: "Month-over-month increase of 28.7% in data transfer costs"
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
const mockServices = ["EC2-Instance", "S3", "RDS", "Lambda", "CloudFront"];
|
|
1193
|
-
const monthlyBreakdown = monthlyCosts.map((cost, i) => {
|
|
1194
|
-
const date = /* @__PURE__ */ new Date();
|
|
1195
|
-
date.setMonth(date.getMonth() - (months - i - 1));
|
|
1196
|
-
const services = {};
|
|
1197
|
-
mockServices.forEach((service) => {
|
|
1198
|
-
services[service] = cost * (0.1 + Math.random() * 0.3);
|
|
1199
|
-
});
|
|
1200
|
-
return {
|
|
1201
|
-
month: date.toISOString().split("T")[0],
|
|
1202
|
-
cost,
|
|
1203
|
-
services
|
|
1204
|
-
};
|
|
1205
|
-
});
|
|
1206
|
-
const serviceTrends = {
|
|
1207
|
-
"EC2-Instance": { currentCost: 850, growthRate: 12.3, trend: "increasing" },
|
|
1208
|
-
"S3": { currentCost: 125, growthRate: -5.2, trend: "decreasing" },
|
|
1209
|
-
"RDS": { currentCost: 420, growthRate: 2.1, trend: "stable" },
|
|
1210
|
-
"Lambda": { currentCost: 45, growthRate: 25.6, trend: "increasing" },
|
|
1211
|
-
"CloudFront": { currentCost: 78, growthRate: -1.8, trend: "stable" }
|
|
1212
|
-
};
|
|
1213
|
-
return {
|
|
1214
|
-
totalCost,
|
|
1215
|
-
averageDailyCost,
|
|
1216
|
-
projectedMonthlyCost,
|
|
1217
|
-
avgMonthOverMonthGrowth,
|
|
1218
|
-
costAnomalies,
|
|
1219
|
-
monthlyBreakdown,
|
|
1220
|
-
serviceTrends,
|
|
1221
|
-
forecastAccuracy: 87
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
async getFinOpsRecommendations() {
|
|
1225
|
-
const recommendations = [
|
|
1226
|
-
{
|
|
1227
|
-
id: "aws-ri-ec2",
|
|
1228
|
-
type: "RESERVED_CAPACITY",
|
|
1229
|
-
title: "Purchase EC2 Reserved Instances",
|
|
1230
|
-
description: "Long-running EC2 instances can benefit from Reserved Instance pricing",
|
|
1231
|
-
potentialSavings: {
|
|
1232
|
-
amount: 500,
|
|
1233
|
-
percentage: 72,
|
|
1234
|
-
timeframe: "MONTHLY"
|
|
1235
|
-
},
|
|
1236
|
-
effort: "LOW",
|
|
1237
|
-
priority: "HIGH",
|
|
1238
|
-
implementationSteps: [
|
|
1239
|
-
"Analyze EC2 usage patterns over the past 30 days",
|
|
1240
|
-
"Identify instances running >75% of the time",
|
|
1241
|
-
"Purchase 1-year or 3-year Reserved Instances",
|
|
1242
|
-
"Monitor utilization and adjust as needed"
|
|
1243
|
-
],
|
|
1244
|
-
tags: ["ec2", "reserved-instances", "cost-optimization"]
|
|
1245
|
-
},
|
|
1246
|
-
{
|
|
1247
|
-
id: "aws-s3-lifecycle",
|
|
1248
|
-
type: "COST_OPTIMIZATION",
|
|
1249
|
-
title: "Implement S3 Lifecycle Policies",
|
|
1250
|
-
description: "Automatically transition old S3 data to cheaper storage classes",
|
|
1251
|
-
potentialSavings: {
|
|
1252
|
-
amount: 150,
|
|
1253
|
-
percentage: 40,
|
|
1254
|
-
timeframe: "MONTHLY"
|
|
1255
|
-
},
|
|
1256
|
-
effort: "MEDIUM",
|
|
1257
|
-
priority: "MEDIUM",
|
|
1258
|
-
implementationSteps: [
|
|
1259
|
-
"Analyze S3 access patterns",
|
|
1260
|
-
"Create lifecycle policies for infrequently accessed data",
|
|
1261
|
-
"Transition to IA after 30 days, Glacier after 90 days",
|
|
1262
|
-
"Enable Intelligent Tiering for dynamic workloads"
|
|
1263
|
-
],
|
|
1264
|
-
tags: ["s3", "lifecycle", "storage-optimization"]
|
|
1265
|
-
},
|
|
1266
|
-
{
|
|
1267
|
-
id: "aws-rightsizing",
|
|
1268
|
-
type: "RESOURCE_RIGHTSIZING",
|
|
1269
|
-
title: "Right-size Underutilized Resources",
|
|
1270
|
-
description: "Reduce costs by downsizing underutilized EC2 and RDS instances",
|
|
1271
|
-
potentialSavings: {
|
|
1272
|
-
amount: 300,
|
|
1273
|
-
percentage: 25,
|
|
1274
|
-
timeframe: "MONTHLY"
|
|
1275
|
-
},
|
|
1276
|
-
effort: "HIGH",
|
|
1277
|
-
priority: "HIGH",
|
|
1278
|
-
implementationSteps: [
|
|
1279
|
-
"Enable CloudWatch detailed monitoring",
|
|
1280
|
-
"Analyze CPU, memory, and network utilization",
|
|
1281
|
-
"Identify instances with <40% average utilization",
|
|
1282
|
-
"Plan maintenance windows for resizing",
|
|
1283
|
-
"Test application performance after changes"
|
|
1284
|
-
],
|
|
1285
|
-
tags: ["rightsizing", "ec2", "rds", "performance-optimization"]
|
|
1286
|
-
}
|
|
1287
|
-
];
|
|
1288
|
-
return recommendations;
|
|
1289
|
-
}
|
|
1290
|
-
// Helper methods for instance types (simplified mapping)
|
|
1291
|
-
getCpuCountForInstanceType(instanceType) {
|
|
1292
|
-
if (!instanceType)
|
|
1293
|
-
return 0;
|
|
1294
|
-
const cpuMap = {
|
|
1295
|
-
"t2.nano": 1,
|
|
1296
|
-
"t2.micro": 1,
|
|
1297
|
-
"t2.small": 1,
|
|
1298
|
-
"t2.medium": 2,
|
|
1299
|
-
"t2.large": 2,
|
|
1300
|
-
"m5.large": 2,
|
|
1301
|
-
"m5.xlarge": 4,
|
|
1302
|
-
"m5.2xlarge": 8,
|
|
1303
|
-
"m5.4xlarge": 16,
|
|
1304
|
-
"c5.large": 2,
|
|
1305
|
-
"c5.xlarge": 4,
|
|
1306
|
-
"c5.2xlarge": 8,
|
|
1307
|
-
"c5.4xlarge": 16
|
|
1308
|
-
};
|
|
1309
|
-
return cpuMap[instanceType] || 1;
|
|
1310
|
-
}
|
|
1311
|
-
getMemoryForInstanceType(instanceType) {
|
|
1312
|
-
if (!instanceType)
|
|
1313
|
-
return 0;
|
|
1314
|
-
const memoryMap = {
|
|
1315
|
-
"t2.nano": 0.5,
|
|
1316
|
-
"t2.micro": 1,
|
|
1317
|
-
"t2.small": 2,
|
|
1318
|
-
"t2.medium": 4,
|
|
1319
|
-
"t2.large": 8,
|
|
1320
|
-
"m5.large": 8,
|
|
1321
|
-
"m5.xlarge": 16,
|
|
1322
|
-
"m5.2xlarge": 32,
|
|
1323
|
-
"m5.4xlarge": 64,
|
|
1324
|
-
"c5.large": 4,
|
|
1325
|
-
"c5.xlarge": 8,
|
|
1326
|
-
"c5.2xlarge": 16,
|
|
1327
|
-
"c5.4xlarge": 32
|
|
1328
|
-
};
|
|
1329
|
-
return memoryMap[instanceType] || 1;
|
|
1330
|
-
}
|
|
1331
|
-
// Helper methods for budget functionality
|
|
1332
|
-
determineBudgetStatus(budget) {
|
|
1333
|
-
if (!budget.CalculatedSpend)
|
|
1334
|
-
return "OK";
|
|
1335
|
-
const actual = parseFloat(budget.CalculatedSpend.ActualSpend?.Amount || "0");
|
|
1336
|
-
const limit = parseFloat(budget.BudgetLimit?.Amount || "0");
|
|
1337
|
-
const forecasted = parseFloat(budget.CalculatedSpend.ForecastedSpend?.Amount || "0");
|
|
1338
|
-
if (actual >= limit)
|
|
1339
|
-
return "ALARM";
|
|
1340
|
-
if (forecasted >= limit)
|
|
1341
|
-
return "FORECASTED_ALARM";
|
|
1342
|
-
return "OK";
|
|
1343
|
-
}
|
|
1344
|
-
parseBudgetThresholds(budget) {
|
|
1345
|
-
return [
|
|
1346
|
-
{
|
|
1347
|
-
threshold: 80,
|
|
1348
|
-
thresholdType: "PERCENTAGE",
|
|
1349
|
-
comparisonOperator: "GREATER_THAN",
|
|
1350
|
-
notificationType: "ACTUAL"
|
|
1351
|
-
},
|
|
1352
|
-
{
|
|
1353
|
-
threshold: 100,
|
|
1354
|
-
thresholdType: "PERCENTAGE",
|
|
1355
|
-
comparisonOperator: "GREATER_THAN",
|
|
1356
|
-
notificationType: "FORECASTED"
|
|
1357
|
-
}
|
|
1358
|
-
];
|
|
1359
|
-
}
|
|
1360
|
-
parseCostFilters(budget) {
|
|
1361
|
-
return budget.CostFilters || {};
|
|
1362
|
-
}
|
|
1363
|
-
calculateTimeRemaining(timePeriod) {
|
|
1364
|
-
if (!timePeriod.end)
|
|
1365
|
-
return "Unknown";
|
|
1366
|
-
const endDate = (0, import_dayjs.default)(timePeriod.end);
|
|
1367
|
-
const now = (0, import_dayjs.default)();
|
|
1368
|
-
const daysRemaining = endDate.diff(now, "day");
|
|
1369
|
-
if (daysRemaining <= 0)
|
|
1370
|
-
return "Period ended";
|
|
1371
|
-
if (daysRemaining === 1)
|
|
1372
|
-
return "1 day";
|
|
1373
|
-
return `${daysRemaining} days`;
|
|
1374
|
-
}
|
|
1375
|
-
determineSeverity(percentageUsed) {
|
|
1376
|
-
if (percentageUsed >= 100)
|
|
1377
|
-
return "CRITICAL";
|
|
1378
|
-
if (percentageUsed >= 90)
|
|
1379
|
-
return "HIGH";
|
|
1380
|
-
if (percentageUsed >= 75)
|
|
1381
|
-
return "MEDIUM";
|
|
1382
|
-
return "LOW";
|
|
1383
|
-
}
|
|
1384
|
-
generateAlertMessage(budget, threshold, percentageUsed) {
|
|
1385
|
-
const thresholdType = threshold.thresholdType === "PERCENTAGE" ? "%" : "$";
|
|
1386
|
-
return `Budget "${budget.budgetName}" has ${threshold.notificationType.toLowerCase()} ${threshold.threshold}${thresholdType} threshold (currently at ${percentageUsed.toFixed(1)}%)`;
|
|
1387
|
-
}
|
|
1388
|
-
async getTopServices(startDate, endDate) {
|
|
1389
|
-
return [
|
|
1390
|
-
{
|
|
1391
|
-
serviceName: "Amazon Elastic Compute Cloud - Compute",
|
|
1392
|
-
cost: 1250.45,
|
|
1393
|
-
percentage: 45.2,
|
|
1394
|
-
trend: "INCREASING"
|
|
1395
|
-
},
|
|
1396
|
-
{
|
|
1397
|
-
serviceName: "Amazon Simple Storage Service",
|
|
1398
|
-
cost: 680.23,
|
|
1399
|
-
percentage: 24.6,
|
|
1400
|
-
trend: "STABLE"
|
|
1401
|
-
},
|
|
1402
|
-
{
|
|
1403
|
-
serviceName: "Amazon Relational Database Service",
|
|
1404
|
-
cost: 445.67,
|
|
1405
|
-
percentage: 16.1,
|
|
1406
|
-
trend: "DECREASING"
|
|
1407
|
-
}
|
|
1408
|
-
];
|
|
1409
|
-
}
|
|
1410
|
-
detectCostAnomalies(trendData) {
|
|
1411
|
-
const anomalies = [];
|
|
1412
|
-
for (let i = 1; i < trendData.length; i++) {
|
|
1413
|
-
const current = trendData[i];
|
|
1414
|
-
const changePercentage = Math.abs(current.changeFromPrevious.percentage);
|
|
1415
|
-
if (changePercentage > 50) {
|
|
1416
|
-
anomalies.push({
|
|
1417
|
-
date: current.period,
|
|
1418
|
-
actualCost: current.actualCost,
|
|
1419
|
-
expectedCost: current.actualCost - current.changeFromPrevious.amount,
|
|
1420
|
-
deviation: changePercentage,
|
|
1421
|
-
severity: changePercentage > 100 ? "HIGH" : "MEDIUM",
|
|
1422
|
-
possibleCause: changePercentage > 0 ? "Unexpected cost increase" : "Significant cost reduction"
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
return anomalies;
|
|
1427
|
-
}
|
|
1428
|
-
static createFromLegacyConfig(legacyConfig) {
|
|
1429
|
-
const config = {
|
|
1430
|
-
provider: "aws" /* AWS */,
|
|
1431
|
-
credentials: {
|
|
1432
|
-
accessKeyId: legacyConfig.credentials.accessKeyId,
|
|
1433
|
-
secretAccessKey: legacyConfig.credentials.secretAccessKey,
|
|
1434
|
-
sessionToken: legacyConfig.credentials.sessionToken
|
|
1435
|
-
},
|
|
1436
|
-
region: legacyConfig.region
|
|
1437
|
-
};
|
|
1438
|
-
return new AWSProvider(config);
|
|
1439
|
-
}
|
|
1440
|
-
calculateVolatility(values) {
|
|
1441
|
-
if (values.length < 2)
|
|
1442
|
-
return 0;
|
|
1443
|
-
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
1444
|
-
const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
|
|
1445
|
-
const stdDev = Math.sqrt(variance);
|
|
1446
|
-
return mean > 0 ? stdDev / mean : 0;
|
|
1447
|
-
}
|
|
1448
|
-
calculateTrendStrength(values) {
|
|
1449
|
-
if (values.length < 3)
|
|
1450
|
-
return 0;
|
|
1451
|
-
const n = values.length;
|
|
1452
|
-
const x = Array.from({ length: n }, (_, i) => i);
|
|
1453
|
-
const meanX = x.reduce((a, b) => a + b, 0) / n;
|
|
1454
|
-
const meanY = values.reduce((a, b) => a + b, 0) / n;
|
|
1455
|
-
const ssXY = x.reduce((sum, xi, i) => sum + (xi - meanX) * (values[i] - meanY), 0);
|
|
1456
|
-
const ssXX = x.reduce((sum, xi) => sum + Math.pow(xi - meanX, 2), 0);
|
|
1457
|
-
const ssYY = values.reduce((sum, yi) => sum + Math.pow(yi - meanY, 2), 0);
|
|
1458
|
-
const correlation = ssXX === 0 || ssYY === 0 ? 0 : ssXY / Math.sqrt(ssXX * ssYY);
|
|
1459
|
-
return Math.abs(correlation);
|
|
1460
|
-
}
|
|
1461
|
-
};
|
|
1462
|
-
|
|
1463
|
-
// src/providers/gcp.ts
|
|
1464
|
-
var GCPProvider = class extends CloudProviderAdapter {
|
|
1465
|
-
constructor(config) {
|
|
1466
|
-
super(config);
|
|
1467
|
-
}
|
|
1468
|
-
async validateCredentials() {
|
|
1469
|
-
console.warn("GCP credential validation not yet implemented");
|
|
1470
|
-
return false;
|
|
1471
|
-
}
|
|
1472
|
-
async getAccountInfo() {
|
|
1473
|
-
showSpinner("Getting GCP project information");
|
|
1474
|
-
try {
|
|
1475
|
-
throw new Error("GCP integration not yet implemented. Please use AWS for now.");
|
|
1476
|
-
} catch (error) {
|
|
1477
|
-
throw new Error(`Failed to get GCP project information: ${error.message}`);
|
|
1478
|
-
}
|
|
1479
|
-
}
|
|
1480
|
-
async getRawCostData() {
|
|
1481
|
-
showSpinner("Getting GCP billing data");
|
|
1482
|
-
try {
|
|
1483
|
-
throw new Error("GCP cost analysis not yet implemented. Please use AWS for now.");
|
|
1484
|
-
} catch (error) {
|
|
1485
|
-
throw new Error(`Failed to get GCP cost data: ${error.message}`);
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
async getCostBreakdown() {
|
|
1489
|
-
const rawCostData = await this.getRawCostData();
|
|
1490
|
-
return this.calculateServiceTotals(rawCostData);
|
|
1491
|
-
}
|
|
1492
|
-
async getResourceInventory(filters) {
|
|
1493
|
-
showSpinner("Discovering GCP resources");
|
|
1494
|
-
const regions = filters?.regions || [this.config.region || "us-central1"];
|
|
1495
|
-
const resourceTypes = filters?.resourceTypes || Object.values(ResourceType);
|
|
1496
|
-
const includeCosts = filters?.includeCosts || false;
|
|
1497
|
-
const inventory = {
|
|
1498
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
1499
|
-
region: regions.join(", "),
|
|
1500
|
-
totalResources: 0,
|
|
1501
|
-
resourcesByType: {
|
|
1502
|
-
["compute" /* COMPUTE */]: 0,
|
|
1503
|
-
["storage" /* STORAGE */]: 0,
|
|
1504
|
-
["database" /* DATABASE */]: 0,
|
|
1505
|
-
["network" /* NETWORK */]: 0,
|
|
1506
|
-
["security" /* SECURITY */]: 0,
|
|
1507
|
-
["serverless" /* SERVERLESS */]: 0,
|
|
1508
|
-
["container" /* CONTAINER */]: 0,
|
|
1509
|
-
["analytics" /* ANALYTICS */]: 0
|
|
1510
|
-
},
|
|
1511
|
-
totalCost: 0,
|
|
1512
|
-
resources: {
|
|
1513
|
-
compute: [],
|
|
1514
|
-
storage: [],
|
|
1515
|
-
database: [],
|
|
1516
|
-
network: [],
|
|
1517
|
-
security: [],
|
|
1518
|
-
serverless: [],
|
|
1519
|
-
container: [],
|
|
1520
|
-
analytics: []
|
|
1521
|
-
},
|
|
1522
|
-
lastUpdated: /* @__PURE__ */ new Date()
|
|
1523
|
-
};
|
|
1524
|
-
const projectId = this.config.credentials.projectId;
|
|
1525
|
-
if (!projectId) {
|
|
1526
|
-
throw new Error("GCP Project ID is required for resource discovery");
|
|
1527
|
-
}
|
|
1528
|
-
try {
|
|
1529
|
-
if (resourceTypes.includes("compute" /* COMPUTE */)) {
|
|
1530
|
-
const computeResources = await this.discoverComputeInstances(projectId, regions, includeCosts);
|
|
1531
|
-
inventory.resources.compute.push(...computeResources);
|
|
1532
|
-
inventory.resourcesByType["compute" /* COMPUTE */] += computeResources.length;
|
|
1533
|
-
}
|
|
1534
|
-
if (resourceTypes.includes("storage" /* STORAGE */)) {
|
|
1535
|
-
const storageResources = await this.discoverStorageBuckets(projectId, includeCosts);
|
|
1536
|
-
inventory.resources.storage.push(...storageResources);
|
|
1537
|
-
inventory.resourcesByType["storage" /* STORAGE */] += storageResources.length;
|
|
1538
|
-
}
|
|
1539
|
-
if (resourceTypes.includes("database" /* DATABASE */)) {
|
|
1540
|
-
const databaseResources = await this.discoverCloudSQLInstances(projectId, regions, includeCosts);
|
|
1541
|
-
inventory.resources.database.push(...databaseResources);
|
|
1542
|
-
inventory.resourcesByType["database" /* DATABASE */] += databaseResources.length;
|
|
1543
|
-
}
|
|
1544
|
-
if (resourceTypes.includes("serverless" /* SERVERLESS */)) {
|
|
1545
|
-
const serverlessResources = await this.discoverCloudFunctions(projectId, regions, includeCosts);
|
|
1546
|
-
inventory.resources.serverless.push(...serverlessResources);
|
|
1547
|
-
inventory.resourcesByType["serverless" /* SERVERLESS */] += serverlessResources.length;
|
|
1548
|
-
}
|
|
1549
|
-
if (resourceTypes.includes("container" /* CONTAINER */)) {
|
|
1550
|
-
const containerResources = await this.discoverGKEClusters(projectId, regions, includeCosts);
|
|
1551
|
-
inventory.resources.container.push(...containerResources);
|
|
1552
|
-
inventory.resourcesByType["container" /* CONTAINER */] += containerResources.length;
|
|
1553
|
-
}
|
|
1554
|
-
} catch (error) {
|
|
1555
|
-
console.warn(`Failed to discover GCP resources: ${error.message}`);
|
|
1556
|
-
}
|
|
1557
|
-
inventory.totalResources = Object.values(inventory.resourcesByType).reduce((sum, count) => sum + count, 0);
|
|
1558
|
-
if (includeCosts) {
|
|
1559
|
-
inventory.totalCost = Object.values(inventory.resources).flat().reduce((sum, resource) => sum + (resource.costToDate || 0), 0);
|
|
1560
|
-
}
|
|
1561
|
-
return inventory;
|
|
1562
|
-
}
|
|
1563
|
-
async discoverComputeInstances(projectId, regions, includeCosts) {
|
|
1564
|
-
console.warn("GCP Compute Engine discovery is simulated - integrate with @google-cloud/compute for production use");
|
|
1565
|
-
const instances = [];
|
|
1566
|
-
if (process.env.GCP_MOCK_DATA === "true") {
|
|
1567
|
-
instances.push({
|
|
1568
|
-
id: "instance-1",
|
|
1569
|
-
name: "web-server-1",
|
|
1570
|
-
state: "RUNNING",
|
|
1571
|
-
region: regions[0],
|
|
1572
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
1573
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-15"),
|
|
1574
|
-
instanceType: "e2-medium",
|
|
1575
|
-
cpu: 1,
|
|
1576
|
-
memory: 4,
|
|
1577
|
-
instanceName: "web-server-1",
|
|
1578
|
-
machineType: "e2-medium",
|
|
1579
|
-
zone: `${regions[0]}-a`,
|
|
1580
|
-
image: "projects/ubuntu-os-cloud/global/images/ubuntu-2004-focal-v20240110",
|
|
1581
|
-
disks: [{
|
|
1582
|
-
type: "pd-standard",
|
|
1583
|
-
sizeGb: 20,
|
|
1584
|
-
boot: true
|
|
1585
|
-
}],
|
|
1586
|
-
networkInterfaces: [{
|
|
1587
|
-
network: "projects/my-project/global/networks/default"
|
|
1588
|
-
}],
|
|
1589
|
-
tags: {
|
|
1590
|
-
"Environment": "production",
|
|
1591
|
-
"Team": "web"
|
|
1592
|
-
},
|
|
1593
|
-
costToDate: includeCosts ? 45.67 : 0
|
|
1594
|
-
});
|
|
1595
|
-
}
|
|
1596
|
-
return instances;
|
|
1597
|
-
}
|
|
1598
|
-
async discoverStorageBuckets(projectId, includeCosts) {
|
|
1599
|
-
console.warn("GCP Cloud Storage discovery is simulated - integrate with @google-cloud/storage for production use");
|
|
1600
|
-
const buckets = [];
|
|
1601
|
-
if (process.env.GCP_MOCK_DATA === "true") {
|
|
1602
|
-
buckets.push({
|
|
1603
|
-
id: "my-app-storage-bucket",
|
|
1604
|
-
name: "my-app-storage-bucket",
|
|
1605
|
-
state: "active",
|
|
1606
|
-
region: "us-central1",
|
|
1607
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
1608
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-10"),
|
|
1609
|
-
sizeGB: 150,
|
|
1610
|
-
storageType: "STANDARD",
|
|
1611
|
-
bucketName: "my-app-storage-bucket",
|
|
1612
|
-
location: "US-CENTRAL1",
|
|
1613
|
-
storageClass: "STANDARD",
|
|
1614
|
-
tags: {
|
|
1615
|
-
"Project": "web-app",
|
|
1616
|
-
"Environment": "production"
|
|
1617
|
-
},
|
|
1618
|
-
costToDate: includeCosts ? 3.45 : 0
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
return buckets;
|
|
1622
|
-
}
|
|
1623
|
-
async discoverCloudSQLInstances(projectId, regions, includeCosts) {
|
|
1624
|
-
console.warn("GCP Cloud SQL discovery is simulated - integrate with Google Cloud SQL Admin API for production use");
|
|
1625
|
-
const instances = [];
|
|
1626
|
-
if (process.env.GCP_MOCK_DATA === "true") {
|
|
1627
|
-
instances.push({
|
|
1628
|
-
id: "production-db-1",
|
|
1629
|
-
name: "production-db-1",
|
|
1630
|
-
state: "RUNNABLE",
|
|
1631
|
-
region: regions[0],
|
|
1632
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
1633
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-12"),
|
|
1634
|
-
engine: "POSTGRES_14",
|
|
1635
|
-
version: "14.9",
|
|
1636
|
-
instanceClass: "db-custom-2-8192",
|
|
1637
|
-
storageGB: 100,
|
|
1638
|
-
instanceId: "production-db-1",
|
|
1639
|
-
databaseVersion: "POSTGRES_14",
|
|
1640
|
-
tier: "db-custom-2-8192",
|
|
1641
|
-
diskSizeGb: 100,
|
|
1642
|
-
diskType: "PD_SSD",
|
|
1643
|
-
ipAddresses: [{
|
|
1644
|
-
type: "PRIMARY",
|
|
1645
|
-
ipAddress: "10.1.2.3"
|
|
1646
|
-
}],
|
|
1647
|
-
tags: {
|
|
1648
|
-
"Database": "primary",
|
|
1649
|
-
"Environment": "production"
|
|
1650
|
-
},
|
|
1651
|
-
costToDate: includeCosts ? 89.23 : 0
|
|
1652
|
-
});
|
|
1653
|
-
}
|
|
1654
|
-
return instances;
|
|
1655
|
-
}
|
|
1656
|
-
async discoverCloudFunctions(projectId, regions, includeCosts) {
|
|
1657
|
-
console.warn("GCP Cloud Functions discovery is simulated - integrate with @google-cloud/functions for production use");
|
|
1658
|
-
const functions = [];
|
|
1659
|
-
if (process.env.GCP_MOCK_DATA === "true") {
|
|
1660
|
-
functions.push({
|
|
1661
|
-
id: "projects/my-project/locations/us-central1/functions/api-handler",
|
|
1662
|
-
name: "api-handler",
|
|
1663
|
-
state: "ACTIVE",
|
|
1664
|
-
region: regions[0],
|
|
1665
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
1666
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-20"),
|
|
1667
|
-
functionName: "api-handler",
|
|
1668
|
-
runtime: "nodejs20",
|
|
1669
|
-
entryPoint: "handleRequest",
|
|
1670
|
-
availableMemoryMb: 256,
|
|
1671
|
-
timeout: "60s",
|
|
1672
|
-
tags: {
|
|
1673
|
-
"Function": "api",
|
|
1674
|
-
"Environment": "production"
|
|
1675
|
-
},
|
|
1676
|
-
costToDate: includeCosts ? 12.45 : 0
|
|
1677
|
-
});
|
|
1678
|
-
}
|
|
1679
|
-
return functions;
|
|
1680
|
-
}
|
|
1681
|
-
async discoverGKEClusters(projectId, regions, includeCosts) {
|
|
1682
|
-
console.warn("GCP GKE discovery is simulated - integrate with @google-cloud/container for production use");
|
|
1683
|
-
const clusters = [];
|
|
1684
|
-
if (process.env.GCP_MOCK_DATA === "true") {
|
|
1685
|
-
clusters.push({
|
|
1686
|
-
id: "production-cluster",
|
|
1687
|
-
name: "production-cluster",
|
|
1688
|
-
state: "RUNNING",
|
|
1689
|
-
region: regions[0],
|
|
1690
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
1691
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-18"),
|
|
1692
|
-
clusterName: "production-cluster",
|
|
1693
|
-
location: `${regions[0]}-a`,
|
|
1694
|
-
nodeCount: 3,
|
|
1695
|
-
currentMasterVersion: "1.28.3-gke.1203001",
|
|
1696
|
-
currentNodeVersion: "1.28.3-gke.1203001",
|
|
1697
|
-
network: "projects/my-project/global/networks/default",
|
|
1698
|
-
nodePools: [{
|
|
1699
|
-
name: "default-pool",
|
|
1700
|
-
nodeCount: 3,
|
|
1701
|
-
config: {
|
|
1702
|
-
machineType: "e2-medium",
|
|
1703
|
-
diskSizeGb: 100
|
|
1704
|
-
}
|
|
1705
|
-
}],
|
|
1706
|
-
tags: {
|
|
1707
|
-
"Cluster": "production",
|
|
1708
|
-
"Environment": "production"
|
|
1709
|
-
},
|
|
1710
|
-
costToDate: includeCosts ? 234.56 : 0
|
|
1711
|
-
});
|
|
1712
|
-
}
|
|
1713
|
-
return clusters;
|
|
1714
|
-
}
|
|
1715
|
-
async getResourceCosts(resourceId) {
|
|
1716
|
-
console.warn("GCP resource costing is not yet implemented - integrate with Google Cloud Billing API");
|
|
1717
|
-
return 0;
|
|
1718
|
-
}
|
|
1719
|
-
async getOptimizationRecommendations() {
|
|
1720
|
-
return [
|
|
1721
|
-
"Consider using Sustained Use Discounts for long-running Compute Engine instances",
|
|
1722
|
-
"Enable Cloud Storage lifecycle policies to automatically transition old data to cheaper storage classes",
|
|
1723
|
-
"Use Committed Use Discounts for predictable Compute Engine workloads to save up to 57%",
|
|
1724
|
-
"Consider using Preemptible VMs for fault-tolerant workloads to save up to 80%",
|
|
1725
|
-
"Review Cloud SQL instances and consider right-sizing based on actual usage",
|
|
1726
|
-
"Use Cloud Functions for event-driven workloads instead of always-on Compute Engine instances",
|
|
1727
|
-
"Implement Cloud Storage Nearline or Coldline for infrequently accessed data",
|
|
1728
|
-
"Consider using Google Kubernetes Engine Autopilot for optimized node management"
|
|
1729
|
-
];
|
|
1730
|
-
}
|
|
1731
|
-
async getBudgets() {
|
|
1732
|
-
throw new Error("GCP budget tracking not yet implemented. Please use AWS for now.");
|
|
1733
|
-
}
|
|
1734
|
-
async getBudgetAlerts() {
|
|
1735
|
-
throw new Error("GCP budget alerts not yet implemented. Please use AWS for now.");
|
|
1736
|
-
}
|
|
1737
|
-
async getCostTrendAnalysis(months) {
|
|
1738
|
-
throw new Error("GCP cost trend analysis not yet implemented. Please use AWS for now.");
|
|
1739
|
-
}
|
|
1740
|
-
async getFinOpsRecommendations() {
|
|
1741
|
-
throw new Error("GCP FinOps recommendations not yet implemented. Please use AWS for now.");
|
|
1742
|
-
}
|
|
1743
|
-
// Helper method to validate GCP-specific configuration
|
|
1744
|
-
static validateGCPConfig(config) {
|
|
1745
|
-
const requiredFields = ["projectId"];
|
|
1746
|
-
for (const field of requiredFields) {
|
|
1747
|
-
if (!config.credentials[field]) {
|
|
1748
|
-
return false;
|
|
1749
|
-
}
|
|
1750
|
-
}
|
|
1751
|
-
return true;
|
|
1752
|
-
}
|
|
1753
|
-
// Helper method to get required credential fields for GCP
|
|
1754
|
-
static getRequiredCredentials() {
|
|
1755
|
-
return [
|
|
1756
|
-
"projectId",
|
|
1757
|
-
"keyFilePath"
|
|
1758
|
-
// Path to service account JSON file
|
|
1759
|
-
// Alternative: 'serviceAccountKey' for JSON content
|
|
1760
|
-
];
|
|
1761
|
-
}
|
|
1762
|
-
};
|
|
1763
|
-
|
|
1764
|
-
// src/providers/azure.ts
|
|
1765
|
-
var AzureProvider = class extends CloudProviderAdapter {
|
|
1766
|
-
constructor(config) {
|
|
1767
|
-
super(config);
|
|
1768
|
-
}
|
|
1769
|
-
async validateCredentials() {
|
|
1770
|
-
console.warn("Azure credential validation not yet implemented");
|
|
1771
|
-
return false;
|
|
1772
|
-
}
|
|
1773
|
-
async getAccountInfo() {
|
|
1774
|
-
showSpinner("Getting Azure subscription information");
|
|
1775
|
-
try {
|
|
1776
|
-
throw new Error("Azure integration not yet implemented. Please use AWS for now.");
|
|
1777
|
-
} catch (error) {
|
|
1778
|
-
throw new Error(`Failed to get Azure subscription information: ${error.message}`);
|
|
1779
|
-
}
|
|
1780
|
-
}
|
|
1781
|
-
async getRawCostData() {
|
|
1782
|
-
showSpinner("Getting Azure cost data");
|
|
1783
|
-
try {
|
|
1784
|
-
throw new Error("Azure cost analysis not yet implemented. Please use AWS for now.");
|
|
1785
|
-
} catch (error) {
|
|
1786
|
-
throw new Error(`Failed to get Azure cost data: ${error.message}`);
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
async getCostBreakdown() {
|
|
1790
|
-
const rawCostData = await this.getRawCostData();
|
|
1791
|
-
return this.calculateServiceTotals(rawCostData);
|
|
1792
|
-
}
|
|
1793
|
-
async getResourceInventory(filters) {
|
|
1794
|
-
showSpinner("Discovering Azure resources");
|
|
1795
|
-
const regions = filters?.regions || [this.config.region || "eastus"];
|
|
1796
|
-
const resourceTypes = filters?.resourceTypes || Object.values(ResourceType);
|
|
1797
|
-
const includeCosts = filters?.includeCosts || false;
|
|
1798
|
-
const inventory = {
|
|
1799
|
-
provider: "azure" /* AZURE */,
|
|
1800
|
-
region: regions.join(", "),
|
|
1801
|
-
totalResources: 0,
|
|
1802
|
-
resourcesByType: {
|
|
1803
|
-
["compute" /* COMPUTE */]: 0,
|
|
1804
|
-
["storage" /* STORAGE */]: 0,
|
|
1805
|
-
["database" /* DATABASE */]: 0,
|
|
1806
|
-
["network" /* NETWORK */]: 0,
|
|
1807
|
-
["security" /* SECURITY */]: 0,
|
|
1808
|
-
["serverless" /* SERVERLESS */]: 0,
|
|
1809
|
-
["container" /* CONTAINER */]: 0,
|
|
1810
|
-
["analytics" /* ANALYTICS */]: 0
|
|
1811
|
-
},
|
|
1812
|
-
totalCost: 0,
|
|
1813
|
-
resources: {
|
|
1814
|
-
compute: [],
|
|
1815
|
-
storage: [],
|
|
1816
|
-
database: [],
|
|
1817
|
-
network: [],
|
|
1818
|
-
security: [],
|
|
1819
|
-
serverless: [],
|
|
1820
|
-
container: [],
|
|
1821
|
-
analytics: []
|
|
1822
|
-
},
|
|
1823
|
-
lastUpdated: /* @__PURE__ */ new Date()
|
|
1824
|
-
};
|
|
1825
|
-
const subscriptionId = this.config.credentials.subscriptionId;
|
|
1826
|
-
if (!subscriptionId) {
|
|
1827
|
-
throw new Error("Azure Subscription ID is required for resource discovery");
|
|
1828
|
-
}
|
|
1829
|
-
try {
|
|
1830
|
-
if (resourceTypes.includes("compute" /* COMPUTE */)) {
|
|
1831
|
-
const computeResources = await this.discoverVirtualMachines(subscriptionId, regions, includeCosts);
|
|
1832
|
-
inventory.resources.compute.push(...computeResources);
|
|
1833
|
-
inventory.resourcesByType["compute" /* COMPUTE */] += computeResources.length;
|
|
1834
|
-
}
|
|
1835
|
-
if (resourceTypes.includes("storage" /* STORAGE */)) {
|
|
1836
|
-
const storageResources = await this.discoverStorageAccounts(subscriptionId, includeCosts);
|
|
1837
|
-
inventory.resources.storage.push(...storageResources);
|
|
1838
|
-
inventory.resourcesByType["storage" /* STORAGE */] += storageResources.length;
|
|
1839
|
-
}
|
|
1840
|
-
if (resourceTypes.includes("database" /* DATABASE */)) {
|
|
1841
|
-
const databaseResources = await this.discoverSQLDatabases(subscriptionId, regions, includeCosts);
|
|
1842
|
-
inventory.resources.database.push(...databaseResources);
|
|
1843
|
-
inventory.resourcesByType["database" /* DATABASE */] += databaseResources.length;
|
|
1844
|
-
}
|
|
1845
|
-
if (resourceTypes.includes("serverless" /* SERVERLESS */)) {
|
|
1846
|
-
const serverlessResources = await this.discoverFunctionApps(subscriptionId, regions, includeCosts);
|
|
1847
|
-
inventory.resources.serverless.push(...serverlessResources);
|
|
1848
|
-
inventory.resourcesByType["serverless" /* SERVERLESS */] += serverlessResources.length;
|
|
1849
|
-
}
|
|
1850
|
-
if (resourceTypes.includes("container" /* CONTAINER */)) {
|
|
1851
|
-
const containerResources = await this.discoverAKSClusters(subscriptionId, regions, includeCosts);
|
|
1852
|
-
inventory.resources.container.push(...containerResources);
|
|
1853
|
-
inventory.resourcesByType["container" /* CONTAINER */] += containerResources.length;
|
|
1854
|
-
}
|
|
1855
|
-
if (resourceTypes.includes("network" /* NETWORK */)) {
|
|
1856
|
-
const networkResources = await this.discoverVirtualNetworks(subscriptionId, regions, includeCosts);
|
|
1857
|
-
inventory.resources.network.push(...networkResources);
|
|
1858
|
-
inventory.resourcesByType["network" /* NETWORK */] += networkResources.length;
|
|
1859
|
-
}
|
|
1860
|
-
} catch (error) {
|
|
1861
|
-
console.warn(`Failed to discover Azure resources: ${error.message}`);
|
|
1862
|
-
}
|
|
1863
|
-
inventory.totalResources = Object.values(inventory.resourcesByType).reduce((sum, count) => sum + count, 0);
|
|
1864
|
-
if (includeCosts) {
|
|
1865
|
-
inventory.totalCost = Object.values(inventory.resources).flat().reduce((sum, resource) => sum + (resource.costToDate || 0), 0);
|
|
1866
|
-
}
|
|
1867
|
-
return inventory;
|
|
1868
|
-
}
|
|
1869
|
-
async discoverVirtualMachines(subscriptionId, regions, includeCosts) {
|
|
1870
|
-
console.warn("Azure Virtual Machines discovery is simulated - integrate with Azure SDK for production use");
|
|
1871
|
-
const vms = [];
|
|
1872
|
-
if (process.env.AZURE_MOCK_DATA === "true") {
|
|
1873
|
-
vms.push({
|
|
1874
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Compute/virtualMachines/web-vm-01",
|
|
1875
|
-
name: "web-vm-01",
|
|
1876
|
-
state: "PowerState/running",
|
|
1877
|
-
region: regions[0],
|
|
1878
|
-
provider: "azure" /* AZURE */,
|
|
1879
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-15"),
|
|
1880
|
-
instanceType: "Standard_D2s_v3",
|
|
1881
|
-
cpu: 2,
|
|
1882
|
-
memory: 8,
|
|
1883
|
-
resourceId: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Compute/virtualMachines/web-vm-01",
|
|
1884
|
-
vmSize: "Standard_D2s_v3",
|
|
1885
|
-
osType: "Linux",
|
|
1886
|
-
imageReference: {
|
|
1887
|
-
publisher: "Canonical",
|
|
1888
|
-
offer: "UbuntuServer",
|
|
1889
|
-
sku: "20.04-LTS",
|
|
1890
|
-
version: "latest"
|
|
1891
|
-
},
|
|
1892
|
-
osDisk: {
|
|
1893
|
-
osType: "Linux",
|
|
1894
|
-
diskSizeGB: 30,
|
|
1895
|
-
managedDisk: {
|
|
1896
|
-
storageAccountType: "Premium_LRS"
|
|
1897
|
-
}
|
|
1898
|
-
},
|
|
1899
|
-
networkProfile: {
|
|
1900
|
-
networkInterfaces: [{
|
|
1901
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Network/networkInterfaces/web-vm-01-nic"
|
|
1902
|
-
}]
|
|
1903
|
-
},
|
|
1904
|
-
tags: {
|
|
1905
|
-
"Environment": "production",
|
|
1906
|
-
"Team": "web",
|
|
1907
|
-
"CostCenter": "engineering"
|
|
1908
|
-
},
|
|
1909
|
-
costToDate: includeCosts ? 89.45 : 0
|
|
1910
|
-
});
|
|
1911
|
-
}
|
|
1912
|
-
return vms;
|
|
1913
|
-
}
|
|
1914
|
-
async discoverStorageAccounts(subscriptionId, includeCosts) {
|
|
1915
|
-
console.warn("Azure Storage Accounts discovery is simulated - integrate with Azure SDK for production use");
|
|
1916
|
-
const storageAccounts = [];
|
|
1917
|
-
if (process.env.AZURE_MOCK_DATA === "true") {
|
|
1918
|
-
storageAccounts.push({
|
|
1919
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/prodstorageacct",
|
|
1920
|
-
name: "prodstorageacct",
|
|
1921
|
-
state: "active",
|
|
1922
|
-
region: "eastus",
|
|
1923
|
-
provider: "azure" /* AZURE */,
|
|
1924
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-10"),
|
|
1925
|
-
sizeGB: 500,
|
|
1926
|
-
storageType: "Standard_LRS",
|
|
1927
|
-
accountName: "prodstorageacct",
|
|
1928
|
-
kind: "StorageV2",
|
|
1929
|
-
tier: "Standard",
|
|
1930
|
-
replicationType: "LRS",
|
|
1931
|
-
accessTier: "Hot",
|
|
1932
|
-
encryption: {
|
|
1933
|
-
services: {
|
|
1934
|
-
blob: { enabled: true },
|
|
1935
|
-
file: { enabled: true }
|
|
1936
|
-
}
|
|
1937
|
-
},
|
|
1938
|
-
tags: {
|
|
1939
|
-
"Environment": "production",
|
|
1940
|
-
"Application": "web-app"
|
|
1941
|
-
},
|
|
1942
|
-
costToDate: includeCosts ? 25.67 : 0
|
|
1943
|
-
});
|
|
1944
|
-
}
|
|
1945
|
-
return storageAccounts;
|
|
1946
|
-
}
|
|
1947
|
-
async discoverSQLDatabases(subscriptionId, regions, includeCosts) {
|
|
1948
|
-
console.warn("Azure SQL Database discovery is simulated - integrate with Azure SQL Management API for production use");
|
|
1949
|
-
const databases = [];
|
|
1950
|
-
if (process.env.AZURE_MOCK_DATA === "true") {
|
|
1951
|
-
databases.push({
|
|
1952
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Sql/servers/prod-sql-server/databases/webapp-db",
|
|
1953
|
-
name: "webapp-db",
|
|
1954
|
-
state: "Online",
|
|
1955
|
-
region: regions[0],
|
|
1956
|
-
provider: "azure" /* AZURE */,
|
|
1957
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-12"),
|
|
1958
|
-
engine: "Microsoft SQL Server",
|
|
1959
|
-
version: "12.0",
|
|
1960
|
-
instanceClass: "S2",
|
|
1961
|
-
storageGB: 250,
|
|
1962
|
-
databaseId: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Sql/servers/prod-sql-server/databases/webapp-db",
|
|
1963
|
-
serverName: "prod-sql-server",
|
|
1964
|
-
edition: "Standard",
|
|
1965
|
-
serviceObjective: "S2",
|
|
1966
|
-
collation: "SQL_Latin1_General_CP1_CI_AS",
|
|
1967
|
-
maxSizeBytes: 268435456e3,
|
|
1968
|
-
status: "Online",
|
|
1969
|
-
elasticPoolName: void 0,
|
|
1970
|
-
tags: {
|
|
1971
|
-
"Database": "primary",
|
|
1972
|
-
"Environment": "production",
|
|
1973
|
-
"Application": "webapp"
|
|
1974
|
-
},
|
|
1975
|
-
costToDate: includeCosts ? 156.78 : 0
|
|
1976
|
-
});
|
|
1977
|
-
}
|
|
1978
|
-
return databases;
|
|
1979
|
-
}
|
|
1980
|
-
async discoverFunctionApps(subscriptionId, regions, includeCosts) {
|
|
1981
|
-
console.warn("Azure Function Apps discovery is simulated - integrate with Azure App Service API for production use");
|
|
1982
|
-
const functionApps = [];
|
|
1983
|
-
if (process.env.AZURE_MOCK_DATA === "true") {
|
|
1984
|
-
functionApps.push({
|
|
1985
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Web/sites/api-functions",
|
|
1986
|
-
name: "api-functions",
|
|
1987
|
-
state: "Running",
|
|
1988
|
-
region: regions[0],
|
|
1989
|
-
provider: "azure" /* AZURE */,
|
|
1990
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-20"),
|
|
1991
|
-
functionAppName: "api-functions",
|
|
1992
|
-
kind: "functionapp",
|
|
1993
|
-
runtime: "dotnet",
|
|
1994
|
-
runtimeVersion: "6",
|
|
1995
|
-
hostingPlan: {
|
|
1996
|
-
name: "consumption-plan",
|
|
1997
|
-
tier: "Dynamic"
|
|
1998
|
-
},
|
|
1999
|
-
tags: {
|
|
2000
|
-
"Function": "api",
|
|
2001
|
-
"Environment": "production"
|
|
2002
|
-
},
|
|
2003
|
-
costToDate: includeCosts ? 23.45 : 0
|
|
2004
|
-
});
|
|
2005
|
-
}
|
|
2006
|
-
return functionApps;
|
|
2007
|
-
}
|
|
2008
|
-
async discoverAKSClusters(subscriptionId, regions, includeCosts) {
|
|
2009
|
-
console.warn("Azure AKS discovery is simulated - integrate with Azure Kubernetes Service API for production use");
|
|
2010
|
-
const clusters = [];
|
|
2011
|
-
if (process.env.AZURE_MOCK_DATA === "true") {
|
|
2012
|
-
clusters.push({
|
|
2013
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.ContainerService/managedClusters/prod-aks",
|
|
2014
|
-
name: "prod-aks",
|
|
2015
|
-
state: "Succeeded",
|
|
2016
|
-
region: regions[0],
|
|
2017
|
-
provider: "azure" /* AZURE */,
|
|
2018
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-18"),
|
|
2019
|
-
clusterName: "prod-aks",
|
|
2020
|
-
kubernetesVersion: "1.28.3",
|
|
2021
|
-
nodeCount: 3,
|
|
2022
|
-
dnsPrefix: "prod-aks-dns",
|
|
2023
|
-
agentPoolProfiles: [{
|
|
2024
|
-
name: "agentpool",
|
|
2025
|
-
count: 3,
|
|
2026
|
-
vmSize: "Standard_D2s_v3",
|
|
2027
|
-
osType: "Linux",
|
|
2028
|
-
osDiskSizeGB: 128
|
|
2029
|
-
}],
|
|
2030
|
-
networkProfile: {
|
|
2031
|
-
networkPlugin: "azure",
|
|
2032
|
-
serviceCidr: "10.0.0.0/16",
|
|
2033
|
-
dnsServiceIP: "10.0.0.10"
|
|
2034
|
-
},
|
|
2035
|
-
tags: {
|
|
2036
|
-
"Cluster": "production",
|
|
2037
|
-
"Environment": "production"
|
|
2038
|
-
},
|
|
2039
|
-
costToDate: includeCosts ? 345.67 : 0
|
|
2040
|
-
});
|
|
2041
|
-
}
|
|
2042
|
-
return clusters;
|
|
2043
|
-
}
|
|
2044
|
-
async discoverVirtualNetworks(subscriptionId, regions, includeCosts) {
|
|
2045
|
-
console.warn("Azure Virtual Networks discovery is simulated - integrate with Azure Network API for production use");
|
|
2046
|
-
const vnets = [];
|
|
2047
|
-
if (process.env.AZURE_MOCK_DATA === "true") {
|
|
2048
|
-
vnets.push({
|
|
2049
|
-
id: "/subscriptions/sub123/resourceGroups/rg-prod/providers/Microsoft.Network/virtualNetworks/prod-vnet",
|
|
2050
|
-
name: "prod-vnet",
|
|
2051
|
-
state: "active",
|
|
2052
|
-
region: regions[0],
|
|
2053
|
-
provider: "azure" /* AZURE */,
|
|
2054
|
-
createdAt: /* @__PURE__ */ new Date("2024-01-05"),
|
|
2055
|
-
vnetName: "prod-vnet",
|
|
2056
|
-
addressSpace: {
|
|
2057
|
-
addressPrefixes: ["10.1.0.0/16"]
|
|
2058
|
-
},
|
|
2059
|
-
subnets: [{
|
|
2060
|
-
name: "default",
|
|
2061
|
-
addressPrefix: "10.1.0.0/24"
|
|
2062
|
-
}, {
|
|
2063
|
-
name: "aks-subnet",
|
|
2064
|
-
addressPrefix: "10.1.1.0/24"
|
|
2065
|
-
}],
|
|
2066
|
-
tags: {
|
|
2067
|
-
"Network": "production",
|
|
2068
|
-
"Environment": "production"
|
|
2069
|
-
},
|
|
2070
|
-
costToDate: includeCosts ? 0 : 0
|
|
2071
|
-
// VNets typically don't have direct costs
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
return vnets;
|
|
2075
|
-
}
|
|
2076
|
-
async getResourceCosts(resourceId) {
|
|
2077
|
-
console.warn("Azure resource costing is not yet implemented - integrate with Azure Cost Management API");
|
|
2078
|
-
return 0;
|
|
2079
|
-
}
|
|
2080
|
-
async getOptimizationRecommendations() {
|
|
2081
|
-
return [
|
|
2082
|
-
"Use Azure Reserved Virtual Machine Instances for consistent workloads to save up to 72%",
|
|
2083
|
-
"Consider Azure Spot Virtual Machines for fault-tolerant workloads to save up to 90%",
|
|
2084
|
-
"Implement Azure Storage lifecycle management to automatically tier data to cooler storage",
|
|
2085
|
-
"Use Azure SQL Database elastic pools for multiple databases with varying usage patterns",
|
|
2086
|
-
"Consider Azure Container Instances for short-lived containerized workloads instead of AKS",
|
|
2087
|
-
"Implement auto-scaling for Virtual Machine Scale Sets to optimize compute costs",
|
|
2088
|
-
"Use Azure Functions consumption plan for event-driven workloads with variable traffic",
|
|
2089
|
-
"Consider Azure Storage Archive tier for long-term retention of infrequently accessed data",
|
|
2090
|
-
"Use Azure Hybrid Benefit to save on Windows Server and SQL Server licensing costs",
|
|
2091
|
-
"Implement Azure Cost Management budgets and alerts to monitor spending"
|
|
2092
|
-
];
|
|
2093
|
-
}
|
|
2094
|
-
async getBudgets() {
|
|
2095
|
-
throw new Error("Azure budget tracking not yet implemented. Please use AWS for now.");
|
|
2096
|
-
}
|
|
2097
|
-
async getBudgetAlerts() {
|
|
2098
|
-
throw new Error("Azure budget alerts not yet implemented. Please use AWS for now.");
|
|
2099
|
-
}
|
|
2100
|
-
async getCostTrendAnalysis(months) {
|
|
2101
|
-
throw new Error("Azure cost trend analysis not yet implemented. Please use AWS for now.");
|
|
2102
|
-
}
|
|
2103
|
-
async getFinOpsRecommendations() {
|
|
2104
|
-
throw new Error("Azure FinOps recommendations not yet implemented. Please use AWS for now.");
|
|
2105
|
-
}
|
|
2106
|
-
// Helper method to validate Azure-specific configuration
|
|
2107
|
-
static validateAzureConfig(config) {
|
|
2108
|
-
const requiredFields = ["subscriptionId", "tenantId", "clientId", "clientSecret"];
|
|
2109
|
-
for (const field of requiredFields) {
|
|
2110
|
-
if (!config.credentials[field]) {
|
|
2111
|
-
return false;
|
|
2112
|
-
}
|
|
2113
|
-
}
|
|
2114
|
-
return true;
|
|
2115
|
-
}
|
|
2116
|
-
// Helper method to get required credential fields for Azure
|
|
2117
|
-
static getRequiredCredentials() {
|
|
2118
|
-
return [
|
|
2119
|
-
"subscriptionId",
|
|
2120
|
-
"tenantId",
|
|
2121
|
-
"clientId",
|
|
2122
|
-
"clientSecret"
|
|
2123
|
-
];
|
|
2124
|
-
}
|
|
2125
|
-
};
|
|
2126
|
-
|
|
2127
|
-
// src/providers/alicloud.ts
|
|
2128
|
-
var AlibabaCloudProvider = class extends CloudProviderAdapter {
|
|
2129
|
-
constructor(config) {
|
|
2130
|
-
super(config);
|
|
2131
|
-
}
|
|
2132
|
-
async validateCredentials() {
|
|
2133
|
-
console.warn("Alibaba Cloud credential validation not yet implemented");
|
|
2134
|
-
return false;
|
|
2135
|
-
}
|
|
2136
|
-
async getAccountInfo() {
|
|
2137
|
-
showSpinner("Getting Alibaba Cloud account information");
|
|
2138
|
-
try {
|
|
2139
|
-
throw new Error("Alibaba Cloud integration not yet implemented. Please use AWS for now.");
|
|
2140
|
-
} catch (error) {
|
|
2141
|
-
throw new Error(`Failed to get Alibaba Cloud account information: ${error.message}`);
|
|
2142
|
-
}
|
|
2143
|
-
}
|
|
2144
|
-
async getRawCostData() {
|
|
2145
|
-
showSpinner("Getting Alibaba Cloud billing data");
|
|
2146
|
-
try {
|
|
2147
|
-
throw new Error("Alibaba Cloud cost analysis not yet implemented. Please use AWS for now.");
|
|
2148
|
-
} catch (error) {
|
|
2149
|
-
throw new Error(`Failed to get Alibaba Cloud cost data: ${error.message}`);
|
|
2150
|
-
}
|
|
2151
|
-
}
|
|
2152
|
-
async getCostBreakdown() {
|
|
2153
|
-
const rawCostData = await this.getRawCostData();
|
|
2154
|
-
return this.calculateServiceTotals(rawCostData);
|
|
2155
|
-
}
|
|
2156
|
-
async getResourceInventory(filters) {
|
|
2157
|
-
throw new Error("Alibaba Cloud resource inventory not yet implemented. Please use AWS for now.");
|
|
2158
|
-
}
|
|
2159
|
-
async getResourceCosts(resourceId) {
|
|
2160
|
-
throw new Error("Alibaba Cloud resource costing not yet implemented. Please use AWS for now.");
|
|
2161
|
-
}
|
|
2162
|
-
async getOptimizationRecommendations() {
|
|
2163
|
-
throw new Error("Alibaba Cloud optimization recommendations not yet implemented. Please use AWS for now.");
|
|
2164
|
-
}
|
|
2165
|
-
async getBudgets() {
|
|
2166
|
-
throw new Error("Alibaba Cloud budget tracking not yet implemented. Please use AWS for now.");
|
|
2167
|
-
}
|
|
2168
|
-
async getBudgetAlerts() {
|
|
2169
|
-
throw new Error("Alibaba Cloud budget alerts not yet implemented. Please use AWS for now.");
|
|
2170
|
-
}
|
|
2171
|
-
async getCostTrendAnalysis(months) {
|
|
2172
|
-
throw new Error("Alibaba Cloud cost trend analysis not yet implemented. Please use AWS for now.");
|
|
2173
|
-
}
|
|
2174
|
-
async getFinOpsRecommendations() {
|
|
2175
|
-
throw new Error("Alibaba Cloud FinOps recommendations not yet implemented. Please use AWS for now.");
|
|
2176
|
-
}
|
|
2177
|
-
// Helper method to validate Alibaba Cloud-specific configuration
|
|
2178
|
-
static validateAlibabaCloudConfig(config) {
|
|
2179
|
-
const requiredFields = ["accessKeyId", "accessKeySecret"];
|
|
2180
|
-
for (const field of requiredFields) {
|
|
2181
|
-
if (!config.credentials[field]) {
|
|
2182
|
-
return false;
|
|
2183
|
-
}
|
|
2184
|
-
}
|
|
2185
|
-
return true;
|
|
2186
|
-
}
|
|
2187
|
-
// Helper method to get required credential fields for Alibaba Cloud
|
|
2188
|
-
static getRequiredCredentials() {
|
|
2189
|
-
return [
|
|
2190
|
-
"accessKeyId",
|
|
2191
|
-
"accessKeySecret"
|
|
2192
|
-
// Optional: 'securityToken', 'regionId'
|
|
2193
|
-
];
|
|
2194
|
-
}
|
|
2195
|
-
};
|
|
2196
|
-
|
|
2197
|
-
// src/providers/oracle.ts
|
|
2198
|
-
var OracleCloudProvider = class extends CloudProviderAdapter {
|
|
2199
|
-
constructor(config) {
|
|
2200
|
-
super(config);
|
|
2201
|
-
}
|
|
2202
|
-
async validateCredentials() {
|
|
2203
|
-
console.warn("Oracle Cloud credential validation not yet implemented");
|
|
2204
|
-
return false;
|
|
2205
|
-
}
|
|
2206
|
-
async getAccountInfo() {
|
|
2207
|
-
showSpinner("Getting Oracle Cloud tenancy information");
|
|
2208
|
-
try {
|
|
2209
|
-
throw new Error("Oracle Cloud integration not yet implemented. Please use AWS for now.");
|
|
2210
|
-
} catch (error) {
|
|
2211
|
-
throw new Error(`Failed to get Oracle Cloud tenancy information: ${error.message}`);
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
async getRawCostData() {
|
|
2215
|
-
showSpinner("Getting Oracle Cloud usage data");
|
|
2216
|
-
try {
|
|
2217
|
-
throw new Error("Oracle Cloud cost analysis not yet implemented. Please use AWS for now.");
|
|
2218
|
-
} catch (error) {
|
|
2219
|
-
throw new Error(`Failed to get Oracle Cloud cost data: ${error.message}`);
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
async getCostBreakdown() {
|
|
2223
|
-
const rawCostData = await this.getRawCostData();
|
|
2224
|
-
return this.calculateServiceTotals(rawCostData);
|
|
2225
|
-
}
|
|
2226
|
-
async getResourceInventory(filters) {
|
|
2227
|
-
throw new Error("Oracle Cloud resource inventory not yet implemented. Please use AWS for now.");
|
|
2228
|
-
}
|
|
2229
|
-
async getResourceCosts(resourceId) {
|
|
2230
|
-
throw new Error("Oracle Cloud resource costing not yet implemented. Please use AWS for now.");
|
|
2231
|
-
}
|
|
2232
|
-
async getOptimizationRecommendations() {
|
|
2233
|
-
throw new Error("Oracle Cloud optimization recommendations not yet implemented. Please use AWS for now.");
|
|
2234
|
-
}
|
|
2235
|
-
async getBudgets() {
|
|
2236
|
-
throw new Error("Oracle Cloud budget tracking not yet implemented. Please use AWS for now.");
|
|
2237
|
-
}
|
|
2238
|
-
async getBudgetAlerts() {
|
|
2239
|
-
throw new Error("Oracle Cloud budget alerts not yet implemented. Please use AWS for now.");
|
|
2240
|
-
}
|
|
2241
|
-
async getCostTrendAnalysis(months) {
|
|
2242
|
-
throw new Error("Oracle Cloud cost trend analysis not yet implemented. Please use AWS for now.");
|
|
2243
|
-
}
|
|
2244
|
-
async getFinOpsRecommendations() {
|
|
2245
|
-
throw new Error("Oracle Cloud FinOps recommendations not yet implemented. Please use AWS for now.");
|
|
2246
|
-
}
|
|
2247
|
-
// Helper method to validate Oracle Cloud-specific configuration
|
|
2248
|
-
static validateOracleCloudConfig(config) {
|
|
2249
|
-
const requiredFields = ["userId", "tenancyId", "fingerprint", "privateKeyPath"];
|
|
2250
|
-
for (const field of requiredFields) {
|
|
2251
|
-
if (!config.credentials[field]) {
|
|
2252
|
-
return false;
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
return true;
|
|
2256
|
-
}
|
|
2257
|
-
// Helper method to get required credential fields for Oracle Cloud
|
|
2258
|
-
static getRequiredCredentials() {
|
|
2259
|
-
return [
|
|
2260
|
-
"userId",
|
|
2261
|
-
// User OCID
|
|
2262
|
-
"tenancyId",
|
|
2263
|
-
// Tenancy OCID
|
|
2264
|
-
"fingerprint",
|
|
2265
|
-
// Public key fingerprint
|
|
2266
|
-
"privateKeyPath"
|
|
2267
|
-
// Path to private key file
|
|
2268
|
-
// Optional: 'region', 'passphrase'
|
|
2269
|
-
];
|
|
2270
|
-
}
|
|
2271
|
-
};
|
|
2272
|
-
|
|
2273
|
-
// src/providers/factory.ts
|
|
2274
|
-
var CloudProviderFactory = class {
|
|
2275
|
-
createProvider(config) {
|
|
2276
|
-
if (!this.validateProviderConfig(config)) {
|
|
2277
|
-
throw new Error(`Invalid configuration for provider: ${config.provider}`);
|
|
2278
|
-
}
|
|
2279
|
-
switch (config.provider) {
|
|
2280
|
-
case "aws" /* AWS */:
|
|
2281
|
-
return new AWSProvider(config);
|
|
2282
|
-
case "gcp" /* GOOGLE_CLOUD */:
|
|
2283
|
-
return new GCPProvider(config);
|
|
2284
|
-
case "azure" /* AZURE */:
|
|
2285
|
-
return new AzureProvider(config);
|
|
2286
|
-
case "alicloud" /* ALIBABA_CLOUD */:
|
|
2287
|
-
return new AlibabaCloudProvider(config);
|
|
2288
|
-
case "oracle" /* ORACLE_CLOUD */:
|
|
2289
|
-
return new OracleCloudProvider(config);
|
|
2290
|
-
default:
|
|
2291
|
-
throw new Error(`Unsupported cloud provider: ${config.provider}`);
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
getSupportedProviders() {
|
|
2295
|
-
return [
|
|
2296
|
-
"aws" /* AWS */,
|
|
2297
|
-
"gcp" /* GOOGLE_CLOUD */,
|
|
2298
|
-
"azure" /* AZURE */,
|
|
2299
|
-
"alicloud" /* ALIBABA_CLOUD */,
|
|
2300
|
-
"oracle" /* ORACLE_CLOUD */
|
|
2301
|
-
];
|
|
2302
|
-
}
|
|
2303
|
-
validateProviderConfig(config) {
|
|
2304
|
-
if (!config.provider) {
|
|
2305
|
-
return false;
|
|
2306
|
-
}
|
|
2307
|
-
const supportedProviders = this.getSupportedProviders();
|
|
2308
|
-
if (!supportedProviders.includes(config.provider)) {
|
|
2309
|
-
return false;
|
|
2310
|
-
}
|
|
2311
|
-
return config.credentials !== null && config.credentials !== void 0;
|
|
2312
|
-
}
|
|
2313
|
-
static getProviderDisplayNames() {
|
|
2314
|
-
return {
|
|
2315
|
-
["aws" /* AWS */]: "Amazon Web Services (AWS)",
|
|
2316
|
-
["gcp" /* GOOGLE_CLOUD */]: "Google Cloud Platform (GCP)",
|
|
2317
|
-
["azure" /* AZURE */]: "Microsoft Azure",
|
|
2318
|
-
["alicloud" /* ALIBABA_CLOUD */]: "Alibaba Cloud",
|
|
2319
|
-
["oracle" /* ORACLE_CLOUD */]: "Oracle Cloud Infrastructure (OCI)"
|
|
2320
|
-
};
|
|
2321
|
-
}
|
|
2322
|
-
static getProviderFromString(provider) {
|
|
2323
|
-
const normalizedProvider = provider.toLowerCase().trim();
|
|
2324
|
-
const providerMap = {
|
|
2325
|
-
"aws": "aws" /* AWS */,
|
|
2326
|
-
"amazon": "aws" /* AWS */,
|
|
2327
|
-
"amazonwebservices": "aws" /* AWS */,
|
|
2328
|
-
"gcp": "gcp" /* GOOGLE_CLOUD */,
|
|
2329
|
-
"google": "gcp" /* GOOGLE_CLOUD */,
|
|
2330
|
-
"googlecloud": "gcp" /* GOOGLE_CLOUD */,
|
|
2331
|
-
"azure": "azure" /* AZURE */,
|
|
2332
|
-
"microsoft": "azure" /* AZURE */,
|
|
2333
|
-
"microsoftazure": "azure" /* AZURE */,
|
|
2334
|
-
"alicloud": "alicloud" /* ALIBABA_CLOUD */,
|
|
2335
|
-
"alibaba": "alicloud" /* ALIBABA_CLOUD */,
|
|
2336
|
-
"alibabacloud": "alicloud" /* ALIBABA_CLOUD */,
|
|
2337
|
-
"oracle": "oracle" /* ORACLE_CLOUD */,
|
|
2338
|
-
"oci": "oracle" /* ORACLE_CLOUD */,
|
|
2339
|
-
"oraclecloud": "oracle" /* ORACLE_CLOUD */
|
|
2340
|
-
};
|
|
2341
|
-
return providerMap[normalizedProvider.replace(/[-_\s]/g, "")] || null;
|
|
2342
|
-
}
|
|
2343
|
-
};
|
|
2344
|
-
|
|
2345
|
-
// src/discovery/profile-discovery.ts
|
|
2346
|
-
var import_fs = require("fs");
|
|
2347
|
-
var import_path = require("path");
|
|
2348
|
-
var import_os = require("os");
|
|
2349
|
-
var import_chalk2 = __toESM(require("chalk"));
|
|
2350
|
-
var CloudProfileDiscovery = class {
|
|
2351
|
-
constructor() {
|
|
2352
|
-
this.warnings = [];
|
|
2353
|
-
this.homeDirectory = (0, import_os.homedir)();
|
|
2354
|
-
}
|
|
2355
|
-
/**
|
|
2356
|
-
* Discover all available cloud provider profiles
|
|
2357
|
-
*/
|
|
2358
|
-
async discoverAllProfiles() {
|
|
2359
|
-
console.log(import_chalk2.default.yellow("\u{1F50D} Discovering cloud provider profiles..."));
|
|
2360
|
-
const results = {
|
|
2361
|
-
totalFound: 0,
|
|
2362
|
-
byProvider: {
|
|
2363
|
-
["aws" /* AWS */]: [],
|
|
2364
|
-
["gcp" /* GOOGLE_CLOUD */]: [],
|
|
2365
|
-
["azure" /* AZURE */]: [],
|
|
2366
|
-
["alicloud" /* ALIBABA_CLOUD */]: [],
|
|
2367
|
-
["oracle" /* ORACLE_CLOUD */]: []
|
|
2368
|
-
},
|
|
2369
|
-
recommended: null,
|
|
2370
|
-
warnings: []
|
|
2371
|
-
};
|
|
2372
|
-
const awsProfiles = await this.discoverAWSProfiles();
|
|
2373
|
-
const gcpProfiles = await this.discoverGCPProfiles();
|
|
2374
|
-
const azureProfiles = await this.discoverAzureProfiles();
|
|
2375
|
-
const alicloudProfiles = await this.discoverAlibabaCloudProfiles();
|
|
2376
|
-
const oracleProfiles = await this.discoverOracleCloudProfiles();
|
|
2377
|
-
results.byProvider["aws" /* AWS */] = awsProfiles;
|
|
2378
|
-
results.byProvider["gcp" /* GOOGLE_CLOUD */] = gcpProfiles;
|
|
2379
|
-
results.byProvider["azure" /* AZURE */] = azureProfiles;
|
|
2380
|
-
results.byProvider["alicloud" /* ALIBABA_CLOUD */] = alicloudProfiles;
|
|
2381
|
-
results.byProvider["oracle" /* ORACLE_CLOUD */] = oracleProfiles;
|
|
2382
|
-
results.totalFound = Object.values(results.byProvider).reduce((total, profiles) => total + profiles.length, 0);
|
|
2383
|
-
results.recommended = this.determineRecommendedProfile(results.byProvider);
|
|
2384
|
-
results.warnings = this.warnings;
|
|
2385
|
-
return results;
|
|
2386
|
-
}
|
|
2387
|
-
/**
|
|
2388
|
-
* Discover AWS profiles from ~/.aws/credentials and ~/.aws/config
|
|
2389
|
-
*/
|
|
2390
|
-
async discoverAWSProfiles() {
|
|
2391
|
-
const profiles = [];
|
|
2392
|
-
const credentialsPath = (0, import_path.join)(this.homeDirectory, ".aws", "credentials");
|
|
2393
|
-
const configPath = (0, import_path.join)(this.homeDirectory, ".aws", "config");
|
|
2394
|
-
if (!(0, import_fs.existsSync)(credentialsPath) && !(0, import_fs.existsSync)(configPath)) {
|
|
2395
|
-
return profiles;
|
|
2396
|
-
}
|
|
2397
|
-
try {
|
|
2398
|
-
const credentialsProfiles = (0, import_fs.existsSync)(credentialsPath) ? this.parseIniFile(credentialsPath) : {};
|
|
2399
|
-
const configProfiles = (0, import_fs.existsSync)(configPath) ? this.parseAWSConfigFile(configPath) : {};
|
|
2400
|
-
const allProfileNames = /* @__PURE__ */ new Set([
|
|
2401
|
-
...Object.keys(credentialsProfiles),
|
|
2402
|
-
...Object.keys(configProfiles)
|
|
2403
|
-
]);
|
|
2404
|
-
for (const profileName of allProfileNames) {
|
|
2405
|
-
const credConfig = credentialsProfiles[profileName] || {};
|
|
2406
|
-
const fileConfig = configProfiles[profileName] || {};
|
|
2407
|
-
if (!credConfig.aws_access_key_id && !fileConfig.role_arn && !fileConfig.sso_start_url) {
|
|
2408
|
-
continue;
|
|
2409
|
-
}
|
|
2410
|
-
profiles.push({
|
|
2411
|
-
name: profileName,
|
|
2412
|
-
provider: "aws" /* AWS */,
|
|
2413
|
-
region: fileConfig.region || credConfig.region || "us-east-1",
|
|
2414
|
-
isDefault: profileName === "default",
|
|
2415
|
-
credentialsPath: (0, import_fs.existsSync)(credentialsPath) ? credentialsPath : void 0,
|
|
2416
|
-
configPath: (0, import_fs.existsSync)(configPath) ? configPath : void 0,
|
|
2417
|
-
status: this.validateAWSProfile(credConfig, fileConfig),
|
|
2418
|
-
lastUsed: this.getLastUsedDate(credentialsPath)
|
|
2419
|
-
});
|
|
2420
|
-
}
|
|
2421
|
-
} catch (error) {
|
|
2422
|
-
this.warnings.push(`Failed to parse AWS profiles: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2423
|
-
}
|
|
2424
|
-
return profiles;
|
|
2425
|
-
}
|
|
2426
|
-
/**
|
|
2427
|
-
* Discover Google Cloud profiles from gcloud CLI
|
|
2428
|
-
*/
|
|
2429
|
-
async discoverGCPProfiles() {
|
|
2430
|
-
const profiles = [];
|
|
2431
|
-
const gcpConfigDir = (0, import_path.join)(this.homeDirectory, ".config", "gcloud");
|
|
2432
|
-
if (!(0, import_fs.existsSync)(gcpConfigDir)) {
|
|
2433
|
-
return profiles;
|
|
2434
|
-
}
|
|
2435
|
-
try {
|
|
2436
|
-
const configurationsPath = (0, import_path.join)(gcpConfigDir, "configurations");
|
|
2437
|
-
if ((0, import_fs.existsSync)(configurationsPath)) {
|
|
2438
|
-
const configFiles = require("fs").readdirSync(configurationsPath);
|
|
2439
|
-
for (const configFile of configFiles) {
|
|
2440
|
-
if (configFile.startsWith("config_")) {
|
|
2441
|
-
const profileName = configFile.replace("config_", "");
|
|
2442
|
-
const configPath = (0, import_path.join)(configurationsPath, configFile);
|
|
2443
|
-
profiles.push({
|
|
2444
|
-
name: profileName,
|
|
2445
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
2446
|
-
isDefault: profileName === "default",
|
|
2447
|
-
configPath,
|
|
2448
|
-
status: "available",
|
|
2449
|
-
lastUsed: this.getLastUsedDate(configPath)
|
|
2450
|
-
});
|
|
2451
|
-
}
|
|
2452
|
-
}
|
|
2453
|
-
}
|
|
2454
|
-
const adcPath = (0, import_path.join)(gcpConfigDir, "application_default_credentials.json");
|
|
2455
|
-
if ((0, import_fs.existsSync)(adcPath)) {
|
|
2456
|
-
profiles.push({
|
|
2457
|
-
name: "application-default",
|
|
2458
|
-
provider: "gcp" /* GOOGLE_CLOUD */,
|
|
2459
|
-
isDefault: true,
|
|
2460
|
-
credentialsPath: adcPath,
|
|
2461
|
-
status: "available"
|
|
2462
|
-
});
|
|
2463
|
-
}
|
|
2464
|
-
} catch (error) {
|
|
2465
|
-
this.warnings.push(`Failed to discover GCP profiles: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2466
|
-
}
|
|
2467
|
-
return profiles;
|
|
2468
|
-
}
|
|
2469
|
-
/**
|
|
2470
|
-
* Discover Azure profiles from Azure CLI
|
|
2471
|
-
*/
|
|
2472
|
-
async discoverAzureProfiles() {
|
|
2473
|
-
const profiles = [];
|
|
2474
|
-
const azureConfigDir = (0, import_path.join)(this.homeDirectory, ".azure");
|
|
2475
|
-
if (!(0, import_fs.existsSync)(azureConfigDir)) {
|
|
2476
|
-
return profiles;
|
|
2477
|
-
}
|
|
2478
|
-
try {
|
|
2479
|
-
const profilesFile = (0, import_path.join)(azureConfigDir, "azureProfile.json");
|
|
2480
|
-
if ((0, import_fs.existsSync)(profilesFile)) {
|
|
2481
|
-
const profilesData = JSON.parse((0, import_fs.readFileSync)(profilesFile, "utf8"));
|
|
2482
|
-
if (profilesData.subscriptions && Array.isArray(profilesData.subscriptions)) {
|
|
2483
|
-
profilesData.subscriptions.forEach((sub, index) => {
|
|
2484
|
-
profiles.push({
|
|
2485
|
-
name: sub.name || `subscription-${index + 1}`,
|
|
2486
|
-
provider: "azure" /* AZURE */,
|
|
2487
|
-
isDefault: sub.isDefault === true,
|
|
2488
|
-
status: sub.state === "Enabled" ? "available" : "invalid",
|
|
2489
|
-
configPath: profilesFile
|
|
2490
|
-
});
|
|
2491
|
-
});
|
|
2492
|
-
}
|
|
2493
|
-
}
|
|
2494
|
-
} catch (error) {
|
|
2495
|
-
this.warnings.push(`Failed to discover Azure profiles: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2496
|
-
}
|
|
2497
|
-
return profiles;
|
|
2498
|
-
}
|
|
2499
|
-
/**
|
|
2500
|
-
* Discover Alibaba Cloud profiles
|
|
2501
|
-
*/
|
|
2502
|
-
async discoverAlibabaCloudProfiles() {
|
|
2503
|
-
const profiles = [];
|
|
2504
|
-
const alicloudConfigPath = (0, import_path.join)(this.homeDirectory, ".aliyun", "config.json");
|
|
2505
|
-
if (!(0, import_fs.existsSync)(alicloudConfigPath)) {
|
|
2506
|
-
return profiles;
|
|
2507
|
-
}
|
|
2508
|
-
try {
|
|
2509
|
-
const configData = JSON.parse((0, import_fs.readFileSync)(alicloudConfigPath, "utf8"));
|
|
2510
|
-
if (configData.profiles && Array.isArray(configData.profiles)) {
|
|
2511
|
-
configData.profiles.forEach((profile) => {
|
|
2512
|
-
profiles.push({
|
|
2513
|
-
name: profile.name || "default",
|
|
2514
|
-
provider: "alicloud" /* ALIBABA_CLOUD */,
|
|
2515
|
-
region: profile.region_id || "cn-hangzhou",
|
|
2516
|
-
isDefault: profile.name === "default",
|
|
2517
|
-
configPath: alicloudConfigPath,
|
|
2518
|
-
status: profile.access_key_id ? "available" : "invalid"
|
|
2519
|
-
});
|
|
2520
|
-
});
|
|
2521
|
-
}
|
|
2522
|
-
} catch (error) {
|
|
2523
|
-
this.warnings.push(`Failed to discover Alibaba Cloud profiles: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2524
|
-
}
|
|
2525
|
-
return profiles;
|
|
2526
|
-
}
|
|
2527
|
-
/**
|
|
2528
|
-
* Discover Oracle Cloud profiles
|
|
2529
|
-
*/
|
|
2530
|
-
async discoverOracleCloudProfiles() {
|
|
2531
|
-
const profiles = [];
|
|
2532
|
-
const ociConfigPath = (0, import_path.join)(this.homeDirectory, ".oci", "config");
|
|
2533
|
-
if (!(0, import_fs.existsSync)(ociConfigPath)) {
|
|
2534
|
-
return profiles;
|
|
2535
|
-
}
|
|
2536
|
-
try {
|
|
2537
|
-
const configProfiles = this.parseIniFile(ociConfigPath);
|
|
2538
|
-
for (const [profileName, config] of Object.entries(configProfiles)) {
|
|
2539
|
-
profiles.push({
|
|
2540
|
-
name: profileName,
|
|
2541
|
-
provider: "oracle" /* ORACLE_CLOUD */,
|
|
2542
|
-
region: config.region || "us-phoenix-1",
|
|
2543
|
-
isDefault: profileName === "DEFAULT",
|
|
2544
|
-
configPath: ociConfigPath,
|
|
2545
|
-
status: config.user && config.key_file ? "available" : "invalid"
|
|
2546
|
-
});
|
|
2547
|
-
}
|
|
2548
|
-
} catch (error) {
|
|
2549
|
-
this.warnings.push(`Failed to discover Oracle Cloud profiles: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2550
|
-
}
|
|
2551
|
-
return profiles;
|
|
2552
|
-
}
|
|
2553
|
-
/**
|
|
2554
|
-
* Display discovered profiles in a formatted table
|
|
2555
|
-
*/
|
|
2556
|
-
displayDiscoveryResults(results) {
|
|
2557
|
-
console.log("\n" + import_chalk2.default.bold.cyan("\u{1F3AF} Profile Discovery Results"));
|
|
2558
|
-
console.log("\u2550".repeat(60));
|
|
2559
|
-
if (results.totalFound === 0) {
|
|
2560
|
-
console.log(import_chalk2.default.yellow("\u26A0\uFE0F No cloud provider profiles found"));
|
|
2561
|
-
console.log(import_chalk2.default.gray(" Make sure you have configured at least one cloud provider CLI"));
|
|
2562
|
-
return;
|
|
2563
|
-
}
|
|
2564
|
-
console.log(import_chalk2.default.green(`\u2705 Found ${results.totalFound} profiles across ${Object.keys(results.byProvider).length} providers`));
|
|
2565
|
-
for (const [provider, profiles] of Object.entries(results.byProvider)) {
|
|
2566
|
-
if (profiles.length > 0) {
|
|
2567
|
-
console.log(`
|
|
2568
|
-
${this.getProviderIcon(provider)} ${import_chalk2.default.bold(provider.toUpperCase())}: ${profiles.length} profiles`);
|
|
2569
|
-
profiles.forEach((profile, index) => {
|
|
2570
|
-
const statusIcon = profile.status === "available" ? "\u2705" : profile.status === "invalid" ? "\u274C" : "\u26A0\uFE0F";
|
|
2571
|
-
const defaultBadge = profile.isDefault ? import_chalk2.default.green(" (default)") : "";
|
|
2572
|
-
console.log(` ${index + 1}. ${profile.name}${defaultBadge} ${statusIcon}`);
|
|
2573
|
-
if (profile.region) {
|
|
2574
|
-
console.log(` Region: ${import_chalk2.default.gray(profile.region)}`);
|
|
2575
|
-
}
|
|
2576
|
-
});
|
|
2577
|
-
}
|
|
2578
|
-
}
|
|
2579
|
-
if (results.recommended) {
|
|
2580
|
-
console.log("\n" + import_chalk2.default.bold.green("\u{1F31F} Recommended Profile:"));
|
|
2581
|
-
console.log(` ${results.recommended.provider.toUpperCase()}: ${results.recommended.name}`);
|
|
2582
|
-
console.log(import_chalk2.default.gray(` Use: --provider ${results.recommended.provider} --profile ${results.recommended.name}`));
|
|
2583
|
-
}
|
|
2584
|
-
if (results.warnings.length > 0) {
|
|
2585
|
-
console.log("\n" + import_chalk2.default.bold.yellow("\u26A0\uFE0F Warnings:"));
|
|
2586
|
-
results.warnings.forEach((warning) => {
|
|
2587
|
-
console.log(import_chalk2.default.yellow(` \u2022 ${warning}`));
|
|
2588
|
-
});
|
|
2589
|
-
}
|
|
2590
|
-
console.log("\n" + import_chalk2.default.bold.cyan("\u{1F4A1} Usage Examples:"));
|
|
2591
|
-
console.log(import_chalk2.default.gray(" infra-cost --provider aws --profile production"));
|
|
2592
|
-
console.log(import_chalk2.default.gray(" infra-cost --provider gcp --profile my-project"));
|
|
2593
|
-
console.log(import_chalk2.default.gray(" infra-cost --all-profiles # Use all available profiles"));
|
|
2594
|
-
}
|
|
2595
|
-
/**
|
|
2596
|
-
* Auto-select best profile based on discovery results
|
|
2597
|
-
*/
|
|
2598
|
-
autoSelectProfile(results) {
|
|
2599
|
-
if (results.recommended && results.recommended.status === "available") {
|
|
2600
|
-
return results.recommended;
|
|
2601
|
-
}
|
|
2602
|
-
for (const profiles of Object.values(results.byProvider)) {
|
|
2603
|
-
const defaultProfile = profiles.find((p) => p.isDefault && p.status === "available");
|
|
2604
|
-
if (defaultProfile)
|
|
2605
|
-
return defaultProfile;
|
|
2606
|
-
}
|
|
2607
|
-
for (const profiles of Object.values(results.byProvider)) {
|
|
2608
|
-
const availableProfile = profiles.find((p) => p.status === "available");
|
|
2609
|
-
if (availableProfile)
|
|
2610
|
-
return availableProfile;
|
|
2611
|
-
}
|
|
2612
|
-
return null;
|
|
2613
|
-
}
|
|
2614
|
-
// Helper methods
|
|
2615
|
-
parseIniFile(filePath) {
|
|
2616
|
-
const content = (0, import_fs.readFileSync)(filePath, "utf8");
|
|
2617
|
-
const profiles = {};
|
|
2618
|
-
let currentProfile = "";
|
|
2619
|
-
content.split("\n").forEach((line) => {
|
|
2620
|
-
line = line.trim();
|
|
2621
|
-
if (line.startsWith("[") && line.endsWith("]")) {
|
|
2622
|
-
currentProfile = line.slice(1, -1);
|
|
2623
|
-
profiles[currentProfile] = {};
|
|
2624
|
-
} else if (currentProfile && line.includes("=")) {
|
|
2625
|
-
const [key, ...valueParts] = line.split("=");
|
|
2626
|
-
profiles[currentProfile][key.trim()] = valueParts.join("=").trim();
|
|
2627
|
-
}
|
|
2628
|
-
});
|
|
2629
|
-
return profiles;
|
|
2630
|
-
}
|
|
2631
|
-
parseAWSConfigFile(filePath) {
|
|
2632
|
-
const content = (0, import_fs.readFileSync)(filePath, "utf8");
|
|
2633
|
-
const profiles = {};
|
|
2634
|
-
let currentProfile = "";
|
|
2635
|
-
content.split("\n").forEach((line) => {
|
|
2636
|
-
line = line.trim();
|
|
2637
|
-
if (line.startsWith("[") && line.endsWith("]")) {
|
|
2638
|
-
const profileMatch = line.match(/\[(?:profile\s+)?(.+)\]/);
|
|
2639
|
-
currentProfile = profileMatch ? profileMatch[1] : "";
|
|
2640
|
-
if (currentProfile) {
|
|
2641
|
-
profiles[currentProfile] = {};
|
|
2642
|
-
}
|
|
2643
|
-
} else if (currentProfile && line.includes("=")) {
|
|
2644
|
-
const [key, ...valueParts] = line.split("=");
|
|
2645
|
-
profiles[currentProfile][key.trim()] = valueParts.join("=").trim();
|
|
2646
|
-
}
|
|
2647
|
-
});
|
|
2648
|
-
return profiles;
|
|
2649
|
-
}
|
|
2650
|
-
validateAWSProfile(credConfig, fileConfig) {
|
|
2651
|
-
if (credConfig.aws_access_key_id && credConfig.aws_secret_access_key) {
|
|
2652
|
-
return "available";
|
|
2653
|
-
}
|
|
2654
|
-
if (fileConfig.role_arn || fileConfig.sso_start_url) {
|
|
2655
|
-
return "available";
|
|
2656
|
-
}
|
|
2657
|
-
return "invalid";
|
|
2658
|
-
}
|
|
2659
|
-
getLastUsedDate(filePath) {
|
|
2660
|
-
try {
|
|
2661
|
-
const stats = require("fs").statSync(filePath);
|
|
2662
|
-
return stats.mtime;
|
|
2663
|
-
} catch {
|
|
2664
|
-
return void 0;
|
|
2665
|
-
}
|
|
2666
|
-
}
|
|
2667
|
-
determineRecommendedProfile(byProvider) {
|
|
2668
|
-
const priority = [
|
|
2669
|
-
"aws" /* AWS */,
|
|
2670
|
-
"gcp" /* GOOGLE_CLOUD */,
|
|
2671
|
-
"azure" /* AZURE */,
|
|
2672
|
-
"alicloud" /* ALIBABA_CLOUD */,
|
|
2673
|
-
"oracle" /* ORACLE_CLOUD */
|
|
2674
|
-
];
|
|
2675
|
-
for (const provider of priority) {
|
|
2676
|
-
const profiles = byProvider[provider];
|
|
2677
|
-
const defaultProfile = profiles.find((p) => p.isDefault && p.status === "available");
|
|
2678
|
-
if (defaultProfile)
|
|
2679
|
-
return defaultProfile;
|
|
2680
|
-
const availableProfile = profiles.find((p) => p.status === "available");
|
|
2681
|
-
if (availableProfile)
|
|
2682
|
-
return availableProfile;
|
|
2683
|
-
}
|
|
2684
|
-
return null;
|
|
2685
|
-
}
|
|
2686
|
-
getProviderIcon(provider) {
|
|
2687
|
-
const icons = {
|
|
2688
|
-
["aws" /* AWS */]: "\u2601\uFE0F",
|
|
2689
|
-
["gcp" /* GOOGLE_CLOUD */]: "\u{1F310}",
|
|
2690
|
-
["azure" /* AZURE */]: "\u{1F537}",
|
|
2691
|
-
["alicloud" /* ALIBABA_CLOUD */]: "\u{1F7E0}",
|
|
2692
|
-
["oracle" /* ORACLE_CLOUD */]: "\u{1F534}"
|
|
2693
|
-
};
|
|
2694
|
-
return icons[provider] || "\u2601\uFE0F";
|
|
2695
|
-
}
|
|
2696
|
-
};
|
|
2697
|
-
|
|
2698
|
-
// src/visualization/terminal-ui.ts
|
|
2699
|
-
var import_cli_table3 = __toESM(require("cli-table3"));
|
|
2700
|
-
var import_chalk3 = __toESM(require("chalk"));
|
|
2701
|
-
var import_cli_progress = __toESM(require("cli-progress"));
|
|
2702
|
-
var import_moment = __toESM(require("moment"));
|
|
2703
|
-
var TerminalUIEngine = class {
|
|
2704
|
-
constructor() {
|
|
2705
|
-
this.progressBar = null;
|
|
2706
|
-
}
|
|
2707
|
-
/**
|
|
2708
|
-
* Creates a formatted table with enhanced styling
|
|
2709
|
-
*/
|
|
2710
|
-
createTable(columns, rows) {
|
|
2711
|
-
const table = new import_cli_table3.default({
|
|
2712
|
-
head: columns.map((col) => import_chalk3.default.bold(col.header)),
|
|
2713
|
-
colWidths: columns.map((col) => col.width || 20),
|
|
2714
|
-
colAligns: columns.map((col) => col.align || "left"),
|
|
2715
|
-
style: {
|
|
2716
|
-
head: [],
|
|
2717
|
-
border: [],
|
|
2718
|
-
compact: false
|
|
2719
|
-
},
|
|
2720
|
-
chars: {
|
|
2721
|
-
"top": "\u2500",
|
|
2722
|
-
"top-mid": "\u252C",
|
|
2723
|
-
"top-left": "\u250C",
|
|
2724
|
-
"top-right": "\u2510",
|
|
2725
|
-
"bottom": "\u2500",
|
|
2726
|
-
"bottom-mid": "\u2534",
|
|
2727
|
-
"bottom-left": "\u2514",
|
|
2728
|
-
"bottom-right": "\u2518",
|
|
2729
|
-
"left": "\u2502",
|
|
2730
|
-
"left-mid": "\u251C",
|
|
2731
|
-
"mid": "\u2500",
|
|
2732
|
-
"mid-mid": "\u253C",
|
|
2733
|
-
"right": "\u2502",
|
|
2734
|
-
"right-mid": "\u2524",
|
|
2735
|
-
"middle": "\u2502"
|
|
2736
|
-
}
|
|
2737
|
-
});
|
|
2738
|
-
rows.forEach((row) => {
|
|
2739
|
-
const formattedRow = columns.map((col, index) => {
|
|
2740
|
-
const value = Object.values(row)[index];
|
|
2741
|
-
const colorKey = col.color;
|
|
2742
|
-
if (colorKey && typeof value === "string") {
|
|
2743
|
-
return import_chalk3.default[colorKey](value);
|
|
2744
|
-
}
|
|
2745
|
-
return String(value);
|
|
2746
|
-
});
|
|
2747
|
-
table.push(formattedRow);
|
|
2748
|
-
});
|
|
2749
|
-
return table.toString();
|
|
2750
|
-
}
|
|
2751
|
-
/**
|
|
2752
|
-
* Creates a cost breakdown table with rich formatting
|
|
2753
|
-
* Optimized for large datasets with pagination and filtering
|
|
2754
|
-
*/
|
|
2755
|
-
createCostTable(costBreakdown, options = {
|
|
2756
|
-
showPercentages: true,
|
|
2757
|
-
highlightTop: 5,
|
|
2758
|
-
currency: "USD",
|
|
2759
|
-
compact: false
|
|
2760
|
-
}) {
|
|
2761
|
-
const { totals, totalsByService } = costBreakdown;
|
|
2762
|
-
let output = "\n" + import_chalk3.default.bold.cyan("\u{1F4B0} Cost Analysis Summary") + "\n";
|
|
2763
|
-
output += "\u2550".repeat(50) + "\n\n";
|
|
2764
|
-
const summaryColumns = [
|
|
2765
|
-
{ header: "Period", width: 15, align: "left", color: "cyan" },
|
|
2766
|
-
{ header: "Cost", width: 15, align: "right", color: "yellow" },
|
|
2767
|
-
{ header: "Change", width: 20, align: "right" }
|
|
2768
|
-
];
|
|
2769
|
-
const summaryRows = [
|
|
2770
|
-
{
|
|
2771
|
-
period: "Yesterday",
|
|
2772
|
-
cost: this.formatCurrency(totals.yesterday, options.currency),
|
|
2773
|
-
change: ""
|
|
2774
|
-
},
|
|
2775
|
-
{
|
|
2776
|
-
period: "Last 7 Days",
|
|
2777
|
-
cost: this.formatCurrency(totals.last7Days, options.currency),
|
|
2778
|
-
change: this.calculateChange(totals.last7Days, totals.yesterday * 7)
|
|
2779
|
-
},
|
|
2780
|
-
{
|
|
2781
|
-
period: "This Month",
|
|
2782
|
-
cost: this.formatCurrency(totals.thisMonth, options.currency),
|
|
2783
|
-
change: this.calculateChange(totals.thisMonth, totals.lastMonth)
|
|
2784
|
-
},
|
|
2785
|
-
{
|
|
2786
|
-
period: "Last Month",
|
|
2787
|
-
cost: this.formatCurrency(totals.lastMonth, options.currency),
|
|
2788
|
-
change: ""
|
|
2789
|
-
}
|
|
2790
|
-
];
|
|
2791
|
-
output += this.createTable(summaryColumns, summaryRows) + "\n\n";
|
|
2792
|
-
output += import_chalk3.default.bold.cyan("\u{1F4CA} Service Breakdown (This Month)") + "\n";
|
|
2793
|
-
output += "\u2550".repeat(50) + "\n\n";
|
|
2794
|
-
const allServiceEntries = Object.entries(totalsByService.thisMonth);
|
|
2795
|
-
const significantServices = allServiceEntries.filter(([_, cost]) => cost > 0.01).sort(([, a], [, b]) => b - a);
|
|
2796
|
-
const maxDisplay = options.highlightTop || 15;
|
|
2797
|
-
const serviceEntries = significantServices.slice(0, maxDisplay);
|
|
2798
|
-
if (allServiceEntries.length > maxDisplay) {
|
|
2799
|
-
const hiddenServices = allServiceEntries.length - maxDisplay;
|
|
2800
|
-
const hiddenCost = significantServices.slice(maxDisplay).reduce((sum, [_, cost]) => sum + cost, 0);
|
|
2801
|
-
if (hiddenCost > 0) {
|
|
2802
|
-
output += import_chalk3.default.gray(`Showing top ${maxDisplay} of ${allServiceEntries.length} services `) + import_chalk3.default.gray(`(${hiddenServices} services with $${hiddenCost.toFixed(2)} hidden)
|
|
2803
|
-
|
|
2804
|
-
`);
|
|
2805
|
-
}
|
|
2806
|
-
}
|
|
2807
|
-
const serviceColumns = [
|
|
2808
|
-
{ header: "Service", width: 25, align: "left", color: "blue" },
|
|
2809
|
-
{ header: "Cost", width: 15, align: "right", color: "yellow" },
|
|
2810
|
-
{ header: "Share", width: 10, align: "right" },
|
|
2811
|
-
{ header: "Trend", width: 15, align: "center" }
|
|
2812
|
-
];
|
|
2813
|
-
const serviceRows = serviceEntries.map(([service, cost]) => {
|
|
2814
|
-
const share = (cost / totals.thisMonth * 100).toFixed(1);
|
|
2815
|
-
const lastMonthCost = totalsByService.lastMonth[service] || 0;
|
|
2816
|
-
const trend = this.getTrendIndicator(cost, lastMonthCost);
|
|
2817
|
-
return {
|
|
2818
|
-
service,
|
|
2819
|
-
cost: this.formatCurrency(cost, options.currency),
|
|
2820
|
-
share: `${share}%`,
|
|
2821
|
-
trend
|
|
2822
|
-
};
|
|
2823
|
-
});
|
|
2824
|
-
output += this.createTable(serviceColumns, serviceRows);
|
|
2825
|
-
return output;
|
|
2826
|
-
}
|
|
2827
|
-
/**
|
|
2828
|
-
* Creates ASCII trend chart for cost visualization
|
|
2829
|
-
*/
|
|
2830
|
-
createTrendChart(trendData, options = {
|
|
2831
|
-
width: 60,
|
|
2832
|
-
showLabels: true,
|
|
2833
|
-
currency: "USD"
|
|
2834
|
-
}) {
|
|
2835
|
-
if (!trendData || trendData.length === 0) {
|
|
2836
|
-
return import_chalk3.default.red("No trend data available");
|
|
2837
|
-
}
|
|
2838
|
-
let output = "\n" + import_chalk3.default.bold.cyan("\u{1F4C8} Cost Trend Analysis") + "\n";
|
|
2839
|
-
output += "\u2550".repeat(50) + "\n\n";
|
|
2840
|
-
const maxCost = Math.max(...trendData.map((d) => d.actualCost));
|
|
2841
|
-
const minCost = Math.min(...trendData.map((d) => d.actualCost));
|
|
2842
|
-
const range = maxCost - minCost;
|
|
2843
|
-
trendData.forEach((data, index) => {
|
|
2844
|
-
const normalizedValue = range > 0 ? (data.actualCost - minCost) / range : 0.5;
|
|
2845
|
-
const barLength = Math.round(normalizedValue * options.width);
|
|
2846
|
-
const bar = "\u2588".repeat(barLength) + "\u2591".repeat(options.width - barLength);
|
|
2847
|
-
const coloredBar = this.colorizeBar(bar, normalizedValue, options.colorThreshold);
|
|
2848
|
-
const period = (0, import_moment.default)(data.period).format("MMM YYYY");
|
|
2849
|
-
const cost = this.formatCurrency(data.actualCost, options.currency);
|
|
2850
|
-
const change = data.changeFromPrevious ? this.formatChangeIndicator(data.changeFromPrevious.percentage) : "";
|
|
2851
|
-
output += `${period.padEnd(10)} ${coloredBar} ${cost.padStart(10)} ${change}
|
|
2852
|
-
`;
|
|
2853
|
-
});
|
|
2854
|
-
output += "\n" + "\u2500".repeat(options.width + 25) + "\n";
|
|
2855
|
-
output += `Range: ${this.formatCurrency(minCost, options.currency)} - ${this.formatCurrency(maxCost, options.currency)}
|
|
2856
|
-
`;
|
|
2857
|
-
return output;
|
|
2858
|
-
}
|
|
2859
|
-
/**
|
|
2860
|
-
* Creates a progress bar for long-running operations
|
|
2861
|
-
*/
|
|
2862
|
-
startProgress(label, total = 100) {
|
|
2863
|
-
if (this.progressBar) {
|
|
2864
|
-
this.progressBar.stop();
|
|
2865
|
-
}
|
|
2866
|
-
this.progressBar = new import_cli_progress.default.SingleBar({
|
|
2867
|
-
format: `${import_chalk3.default.cyan(label)} ${import_chalk3.default.cyan("[")}${import_chalk3.default.yellow("{bar}")}${import_chalk3.default.cyan("]")} {percentage}% | ETA: {eta}s | {value}/{total}`,
|
|
2868
|
-
barCompleteChar: "\u2588",
|
|
2869
|
-
barIncompleteChar: "\u2591",
|
|
2870
|
-
hideCursor: true
|
|
2871
|
-
});
|
|
2872
|
-
this.progressBar.start(total, 0);
|
|
2873
|
-
}
|
|
2874
|
-
/**
|
|
2875
|
-
* Updates progress bar
|
|
2876
|
-
*/
|
|
2877
|
-
updateProgress(value, payload) {
|
|
2878
|
-
if (this.progressBar) {
|
|
2879
|
-
this.progressBar.update(value, payload);
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
/**
|
|
2883
|
-
* Stops and clears progress bar
|
|
2884
|
-
*/
|
|
2885
|
-
stopProgress() {
|
|
2886
|
-
if (this.progressBar) {
|
|
2887
|
-
this.progressBar.stop();
|
|
2888
|
-
this.progressBar = null;
|
|
2889
|
-
}
|
|
2890
|
-
}
|
|
2891
|
-
/**
|
|
2892
|
-
* Creates a cost anomaly alert box
|
|
2893
|
-
*/
|
|
2894
|
-
createAnomalyAlert(anomalies) {
|
|
2895
|
-
if (anomalies.length === 0) {
|
|
2896
|
-
return import_chalk3.default.green("\u2705 No cost anomalies detected");
|
|
2897
|
-
}
|
|
2898
|
-
let output = "\n" + import_chalk3.default.bold.red("\u{1F6A8} Cost Anomalies Detected") + "\n";
|
|
2899
|
-
output += "\u2550".repeat(50) + "\n\n";
|
|
2900
|
-
anomalies.forEach((anomaly) => {
|
|
2901
|
-
const severityColor = this.getSeverityColor(anomaly.severity);
|
|
2902
|
-
const icon = this.getSeverityIcon(anomaly.severity);
|
|
2903
|
-
output += import_chalk3.default[severityColor](`${icon} ${anomaly.date}
|
|
2904
|
-
`);
|
|
2905
|
-
output += ` Expected: ${this.formatCurrency(anomaly.expectedCost, "USD")}
|
|
2906
|
-
`;
|
|
2907
|
-
output += ` Actual: ${this.formatCurrency(anomaly.actualCost, "USD")}
|
|
2908
|
-
`;
|
|
2909
|
-
output += ` Deviation: ${anomaly.deviation > 0 ? "+" : ""}${anomaly.deviation.toFixed(1)}%
|
|
2910
|
-
`;
|
|
2911
|
-
if (anomaly.description) {
|
|
2912
|
-
output += ` ${import_chalk3.default.gray(anomaly.description)}
|
|
2913
|
-
`;
|
|
2914
|
-
}
|
|
2915
|
-
output += "\n";
|
|
2916
|
-
});
|
|
2917
|
-
return output;
|
|
2918
|
-
}
|
|
2919
|
-
/**
|
|
2920
|
-
* Creates a fancy header with branding
|
|
2921
|
-
*/
|
|
2922
|
-
createHeader(title, subtitle) {
|
|
2923
|
-
const width = 60;
|
|
2924
|
-
let output = "\n";
|
|
2925
|
-
output += import_chalk3.default.cyan("\u250C" + "\u2500".repeat(width - 2) + "\u2510") + "\n";
|
|
2926
|
-
const titlePadding = Math.floor((width - title.length - 4) / 2);
|
|
2927
|
-
output += import_chalk3.default.cyan("\u2502") + " ".repeat(titlePadding) + import_chalk3.default.bold.white(title) + " ".repeat(width - title.length - titlePadding - 2) + import_chalk3.default.cyan("\u2502") + "\n";
|
|
2928
|
-
if (subtitle) {
|
|
2929
|
-
const subtitlePadding = Math.floor((width - subtitle.length - 4) / 2);
|
|
2930
|
-
output += import_chalk3.default.cyan("\u2502") + " ".repeat(subtitlePadding) + import_chalk3.default.gray(subtitle) + " ".repeat(width - subtitle.length - subtitlePadding - 2) + import_chalk3.default.cyan("\u2502") + "\n";
|
|
2931
|
-
}
|
|
2932
|
-
output += import_chalk3.default.cyan("\u2514" + "\u2500".repeat(width - 2) + "\u2518") + "\n\n";
|
|
2933
|
-
return output;
|
|
2934
|
-
}
|
|
2935
|
-
// Helper methods
|
|
2936
|
-
formatCurrency(amount, currency) {
|
|
2937
|
-
return new Intl.NumberFormat("en-US", {
|
|
2938
|
-
style: "currency",
|
|
2939
|
-
currency,
|
|
2940
|
-
minimumFractionDigits: 2,
|
|
2941
|
-
maximumFractionDigits: 2
|
|
2942
|
-
}).format(amount);
|
|
2943
|
-
}
|
|
2944
|
-
calculateChange(current, previous) {
|
|
2945
|
-
if (previous === 0)
|
|
2946
|
-
return "";
|
|
2947
|
-
const change = (current - previous) / previous * 100;
|
|
2948
|
-
const changeStr = `${change >= 0 ? "+" : ""}${change.toFixed(1)}%`;
|
|
2949
|
-
if (change > 0) {
|
|
2950
|
-
return import_chalk3.default.red(`\u2197 ${changeStr}`);
|
|
2951
|
-
} else if (change < 0) {
|
|
2952
|
-
return import_chalk3.default.green(`\u2198 ${changeStr}`);
|
|
2953
|
-
} else {
|
|
2954
|
-
return import_chalk3.default.gray("\u2192 0.0%");
|
|
2955
|
-
}
|
|
2956
|
-
}
|
|
2957
|
-
getTrendIndicator(current, previous) {
|
|
2958
|
-
if (previous === 0)
|
|
2959
|
-
return import_chalk3.default.gray("\u2500");
|
|
2960
|
-
const change = (current - previous) / previous * 100;
|
|
2961
|
-
if (change > 10)
|
|
2962
|
-
return import_chalk3.default.red("\u2197\u2197");
|
|
2963
|
-
if (change > 0)
|
|
2964
|
-
return import_chalk3.default.yellow("\u2197");
|
|
2965
|
-
if (change < -10)
|
|
2966
|
-
return import_chalk3.default.green("\u2198\u2198");
|
|
2967
|
-
if (change < 0)
|
|
2968
|
-
return import_chalk3.default.green("\u2198");
|
|
2969
|
-
return import_chalk3.default.gray("\u2192");
|
|
2970
|
-
}
|
|
2971
|
-
colorizeBar(bar, normalizedValue, threshold) {
|
|
2972
|
-
const thresholdValue = threshold || 0.7;
|
|
2973
|
-
if (normalizedValue > thresholdValue) {
|
|
2974
|
-
return import_chalk3.default.red(bar);
|
|
2975
|
-
} else if (normalizedValue > 0.4) {
|
|
2976
|
-
return import_chalk3.default.yellow(bar);
|
|
2977
|
-
} else {
|
|
2978
|
-
return import_chalk3.default.green(bar);
|
|
2979
|
-
}
|
|
2980
|
-
}
|
|
2981
|
-
formatChangeIndicator(percentage) {
|
|
2982
|
-
const indicator = percentage >= 0 ? "\u2197" : "\u2198";
|
|
2983
|
-
const color = percentage >= 0 ? "red" : "green";
|
|
2984
|
-
const sign = percentage >= 0 ? "+" : "";
|
|
2985
|
-
return import_chalk3.default[color](`${indicator} ${sign}${percentage.toFixed(1)}%`);
|
|
2986
|
-
}
|
|
2987
|
-
getSeverityColor(severity) {
|
|
2988
|
-
switch (severity.toLowerCase()) {
|
|
2989
|
-
case "critical":
|
|
2990
|
-
return "red";
|
|
2991
|
-
case "high":
|
|
2992
|
-
return "red";
|
|
2993
|
-
case "medium":
|
|
2994
|
-
return "yellow";
|
|
2995
|
-
case "low":
|
|
2996
|
-
return "cyan";
|
|
2997
|
-
default:
|
|
2998
|
-
return "gray";
|
|
2999
|
-
}
|
|
3000
|
-
}
|
|
3001
|
-
getSeverityIcon(severity) {
|
|
3002
|
-
switch (severity.toLowerCase()) {
|
|
3003
|
-
case "critical":
|
|
3004
|
-
return "\u{1F534}";
|
|
3005
|
-
case "high":
|
|
3006
|
-
return "\u{1F7E0}";
|
|
3007
|
-
case "medium":
|
|
3008
|
-
return "\u{1F7E1}";
|
|
3009
|
-
case "low":
|
|
3010
|
-
return "\u{1F535}";
|
|
3011
|
-
default:
|
|
3012
|
-
return "\u26AA";
|
|
3013
|
-
}
|
|
3014
|
-
}
|
|
3015
|
-
};
|
|
3016
|
-
|
|
3017
|
-
// src/visualization/multi-cloud-dashboard.ts
|
|
3018
|
-
var MultiCloudDashboard = class {
|
|
3019
|
-
constructor() {
|
|
3020
|
-
this.ui = new TerminalUIEngine();
|
|
3021
|
-
this.factory = new CloudProviderFactory();
|
|
3022
|
-
this.discovery = new CloudProfileDiscovery();
|
|
3023
|
-
}
|
|
3024
|
-
/**
|
|
3025
|
-
* Create comprehensive multi-cloud inventory dashboard
|
|
3026
|
-
*/
|
|
3027
|
-
async generateMultiCloudInventoryDashboard(providers) {
|
|
3028
|
-
console.log(import_chalk4.default.yellow("\u{1F310} Gathering multi-cloud inventory..."));
|
|
3029
|
-
const summary = await this.collectMultiCloudInventory(providers);
|
|
3030
|
-
return this.renderMultiCloudDashboard(summary);
|
|
3031
|
-
}
|
|
3032
|
-
/**
|
|
3033
|
-
* Collect inventory from all available cloud providers
|
|
3034
|
-
*/
|
|
3035
|
-
async collectMultiCloudInventory(providers) {
|
|
3036
|
-
const summary = {
|
|
3037
|
-
totalProviders: 0,
|
|
3038
|
-
totalResources: 0,
|
|
3039
|
-
totalCost: 0,
|
|
3040
|
-
providerBreakdown: {},
|
|
3041
|
-
consolidatedResourcesByType: {
|
|
3042
|
-
["compute" /* COMPUTE */]: 0,
|
|
3043
|
-
["storage" /* STORAGE */]: 0,
|
|
3044
|
-
["database" /* DATABASE */]: 0,
|
|
3045
|
-
["network" /* NETWORK */]: 0,
|
|
3046
|
-
["security" /* SECURITY */]: 0,
|
|
3047
|
-
["serverless" /* SERVERLESS */]: 0,
|
|
3048
|
-
["container" /* CONTAINER */]: 0,
|
|
3049
|
-
["analytics" /* ANALYTICS */]: 0
|
|
3050
|
-
},
|
|
3051
|
-
topResourcesByProvider: []
|
|
3052
|
-
};
|
|
3053
|
-
const discoveryResults = await this.discovery.discoverAllProfiles();
|
|
3054
|
-
const targetProviders = providers || this.factory.getSupportedProviders();
|
|
3055
|
-
for (const provider of targetProviders) {
|
|
3056
|
-
const profiles = discoveryResults.byProvider[provider];
|
|
3057
|
-
summary.providerBreakdown[provider] = {
|
|
3058
|
-
inventory: null,
|
|
3059
|
-
status: "unavailable",
|
|
3060
|
-
resourceCount: 0,
|
|
3061
|
-
cost: 0
|
|
3062
|
-
};
|
|
3063
|
-
if (profiles.length === 0) {
|
|
3064
|
-
continue;
|
|
3065
|
-
}
|
|
3066
|
-
try {
|
|
3067
|
-
const profile = profiles.find((p) => p.status === "available");
|
|
3068
|
-
if (!profile) {
|
|
3069
|
-
summary.providerBreakdown[provider].status = "unavailable";
|
|
3070
|
-
continue;
|
|
3071
|
-
}
|
|
3072
|
-
console.log(import_chalk4.default.gray(` Scanning ${provider.toUpperCase()} resources...`));
|
|
3073
|
-
const providerAdapter = this.createProviderAdapter(provider, profile);
|
|
3074
|
-
if (!providerAdapter) {
|
|
3075
|
-
summary.providerBreakdown[provider].status = "error";
|
|
3076
|
-
summary.providerBreakdown[provider].errorMessage = "Failed to initialize provider";
|
|
3077
|
-
continue;
|
|
3078
|
-
}
|
|
3079
|
-
const inventory = await providerAdapter.getResourceInventory({
|
|
3080
|
-
includeCosts: true,
|
|
3081
|
-
resourceTypes: Object.values(ResourceType)
|
|
3082
|
-
});
|
|
3083
|
-
summary.providerBreakdown[provider] = {
|
|
3084
|
-
inventory,
|
|
3085
|
-
status: "active",
|
|
3086
|
-
resourceCount: inventory.totalResources,
|
|
3087
|
-
cost: inventory.totalCost
|
|
3088
|
-
};
|
|
3089
|
-
summary.totalProviders++;
|
|
3090
|
-
summary.totalResources += inventory.totalResources;
|
|
3091
|
-
summary.totalCost += inventory.totalCost;
|
|
3092
|
-
Object.entries(inventory.resourcesByType).forEach(([type, count]) => {
|
|
3093
|
-
summary.consolidatedResourcesByType[type] += count;
|
|
3094
|
-
});
|
|
3095
|
-
} catch (error) {
|
|
3096
|
-
console.warn(import_chalk4.default.yellow(`\u26A0\uFE0F Failed to scan ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`));
|
|
3097
|
-
summary.providerBreakdown[provider].status = "error";
|
|
3098
|
-
summary.providerBreakdown[provider].errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
3099
|
-
}
|
|
3100
|
-
}
|
|
3101
|
-
summary.topResourcesByProvider = this.calculateTopResourcesByProvider(summary);
|
|
3102
|
-
return summary;
|
|
3103
|
-
}
|
|
3104
|
-
/**
|
|
3105
|
-
* Render the multi-cloud dashboard
|
|
3106
|
-
*/
|
|
3107
|
-
renderMultiCloudDashboard(summary) {
|
|
3108
|
-
let output = "";
|
|
3109
|
-
output += this.ui.createHeader(
|
|
3110
|
-
"\u{1F310} Multi-Cloud Infrastructure Dashboard",
|
|
3111
|
-
`Comprehensive view across ${summary.totalProviders} cloud providers`
|
|
3112
|
-
);
|
|
3113
|
-
output += import_chalk4.default.bold.cyan("\u{1F4CA} Executive Summary") + "\n";
|
|
3114
|
-
output += "\u2550".repeat(60) + "\n\n";
|
|
3115
|
-
const summaryTable = this.ui.createTable([
|
|
3116
|
-
{ header: "Metric", width: 25, align: "left", color: "cyan" },
|
|
3117
|
-
{ header: "Value", width: 20, align: "right", color: "yellow" },
|
|
3118
|
-
{ header: "Details", width: 30, align: "left" }
|
|
3119
|
-
], [
|
|
3120
|
-
{
|
|
3121
|
-
metric: "Active Providers",
|
|
3122
|
-
value: summary.totalProviders.toString(),
|
|
3123
|
-
details: this.getActiveProvidersList(summary)
|
|
3124
|
-
},
|
|
3125
|
-
{
|
|
3126
|
-
metric: "Total Resources",
|
|
3127
|
-
value: summary.totalResources.toLocaleString(),
|
|
3128
|
-
details: this.getResourceTypeBreakdown(summary)
|
|
3129
|
-
},
|
|
3130
|
-
{
|
|
3131
|
-
metric: "Total Monthly Cost",
|
|
3132
|
-
value: `$${summary.totalCost.toFixed(2)}`,
|
|
3133
|
-
details: this.getCostBreakdownByProvider(summary)
|
|
3134
|
-
}
|
|
3135
|
-
]);
|
|
3136
|
-
output += summaryTable + "\n\n";
|
|
3137
|
-
output += import_chalk4.default.bold.cyan("\u2601\uFE0F Provider Breakdown") + "\n";
|
|
3138
|
-
output += "\u2550".repeat(60) + "\n\n";
|
|
3139
|
-
for (const [provider, data] of Object.entries(summary.providerBreakdown)) {
|
|
3140
|
-
const providerName = this.getProviderDisplayName(provider);
|
|
3141
|
-
const statusIcon = this.getStatusIcon(data.status);
|
|
3142
|
-
const statusColor = this.getStatusColor(data.status);
|
|
3143
|
-
output += `${statusIcon} ${import_chalk4.default.bold[statusColor](providerName)}
|
|
3144
|
-
`;
|
|
3145
|
-
if (data.status === "active" && data.inventory) {
|
|
3146
|
-
output += ` Resources: ${import_chalk4.default.yellow(data.resourceCount.toLocaleString())}
|
|
3147
|
-
`;
|
|
3148
|
-
output += ` Cost: ${import_chalk4.default.green(`$${data.cost.toFixed(2)}`)}
|
|
3149
|
-
`;
|
|
3150
|
-
output += ` Regions: ${import_chalk4.default.blue(data.inventory.region)}
|
|
3151
|
-
`;
|
|
3152
|
-
output += ` Last Updated: ${import_chalk4.default.gray(data.inventory.lastUpdated.toLocaleString())}
|
|
3153
|
-
`;
|
|
3154
|
-
const topTypes = Object.entries(data.inventory.resourcesByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a).slice(0, 3).map(([type, count]) => `${count} ${type}`).join(", ");
|
|
3155
|
-
if (topTypes) {
|
|
3156
|
-
output += ` Top Types: ${import_chalk4.default.gray(topTypes)}
|
|
3157
|
-
`;
|
|
3158
|
-
}
|
|
3159
|
-
} else if (data.status === "error") {
|
|
3160
|
-
output += ` ${import_chalk4.default.red("Error: " + (data.errorMessage || "Unknown error"))}
|
|
3161
|
-
`;
|
|
3162
|
-
} else if (data.status === "unavailable") {
|
|
3163
|
-
output += ` ${import_chalk4.default.gray("No credentials or profiles configured")}
|
|
3164
|
-
`;
|
|
3165
|
-
}
|
|
3166
|
-
output += "\n";
|
|
3167
|
-
}
|
|
3168
|
-
output += import_chalk4.default.bold.cyan("\u{1F4C8} Consolidated Resource Analysis") + "\n";
|
|
3169
|
-
output += "\u2550".repeat(60) + "\n\n";
|
|
3170
|
-
const resourceTypeTable = this.ui.createTable([
|
|
3171
|
-
{ header: "Resource Type", width: 20, align: "left", color: "blue" },
|
|
3172
|
-
{ header: "Total Count", width: 15, align: "right", color: "yellow" },
|
|
3173
|
-
{ header: "Percentage", width: 12, align: "right" },
|
|
3174
|
-
{ header: "Top Provider", width: 20, align: "left" }
|
|
3175
|
-
], this.createResourceTypeRows(summary));
|
|
3176
|
-
output += resourceTypeTable + "\n\n";
|
|
3177
|
-
output += this.generateMultiCloudInsights(summary);
|
|
3178
|
-
output += import_chalk4.default.bold.cyan("\u{1F4A1} Multi-Cloud Recommendations") + "\n";
|
|
3179
|
-
output += "\u2550".repeat(60) + "\n";
|
|
3180
|
-
output += this.generateMultiCloudRecommendations(summary);
|
|
3181
|
-
return output;
|
|
3182
|
-
}
|
|
3183
|
-
/**
|
|
3184
|
-
* Generate actionable multi-cloud insights
|
|
3185
|
-
*/
|
|
3186
|
-
generateMultiCloudInsights(summary) {
|
|
3187
|
-
let output = import_chalk4.default.bold.cyan("\u{1F9E0} Multi-Cloud Insights") + "\n";
|
|
3188
|
-
output += "\u2550".repeat(60) + "\n\n";
|
|
3189
|
-
const insights = [];
|
|
3190
|
-
const activeProviders = Object.values(summary.providerBreakdown).filter((p) => p.status === "active").length;
|
|
3191
|
-
if (activeProviders === 1) {
|
|
3192
|
-
insights.push("\u{1F4CD} Single-cloud deployment detected - consider multi-cloud strategy for resilience");
|
|
3193
|
-
} else if (activeProviders > 3) {
|
|
3194
|
-
insights.push("\u{1F30D} Excellent cloud diversity - you have strong vendor independence");
|
|
3195
|
-
}
|
|
3196
|
-
const providerCosts = Object.entries(summary.providerBreakdown).filter(([, data]) => data.status === "active").map(([provider, data]) => ({ provider, cost: data.cost })).sort((a, b) => b.cost - a.cost);
|
|
3197
|
-
if (providerCosts.length > 1) {
|
|
3198
|
-
const topProvider = providerCosts[0];
|
|
3199
|
-
const costPercentage = topProvider.cost / summary.totalCost * 100;
|
|
3200
|
-
if (costPercentage > 70) {
|
|
3201
|
-
insights.push(`\u{1F4B0} ${topProvider.provider.toUpperCase()} dominates ${costPercentage.toFixed(1)}% of costs - consider rebalancing`);
|
|
3202
|
-
}
|
|
3203
|
-
}
|
|
3204
|
-
const topResourceType = Object.entries(summary.consolidatedResourcesByType).sort(([, a], [, b]) => b - a)[0];
|
|
3205
|
-
if (topResourceType && topResourceType[1] > 0) {
|
|
3206
|
-
const percentage = topResourceType[1] / summary.totalResources * 100;
|
|
3207
|
-
insights.push(`\u{1F527} ${topResourceType[0]} resources account for ${percentage.toFixed(1)}% of total infrastructure`);
|
|
3208
|
-
}
|
|
3209
|
-
insights.forEach((insight, index) => {
|
|
3210
|
-
output += `${index + 1}. ${insight}
|
|
3211
|
-
`;
|
|
3212
|
-
});
|
|
3213
|
-
return output + "\n";
|
|
3214
|
-
}
|
|
3215
|
-
/**
|
|
3216
|
-
* Generate multi-cloud optimization recommendations
|
|
3217
|
-
*/
|
|
3218
|
-
generateMultiCloudRecommendations(summary) {
|
|
3219
|
-
let output = "";
|
|
3220
|
-
const recommendations = [];
|
|
3221
|
-
const activeProviders = Object.entries(summary.providerBreakdown).filter(([, data]) => data.status === "active");
|
|
3222
|
-
if (activeProviders.length > 1) {
|
|
3223
|
-
recommendations.push("\u{1F504} Use --compare-clouds to identify cost arbitrage opportunities");
|
|
3224
|
-
recommendations.push("\u{1F4CA} Run --optimization-report for cross-cloud resource rightsizing");
|
|
3225
|
-
}
|
|
3226
|
-
const unavailableProviders = Object.entries(summary.providerBreakdown).filter(([, data]) => data.status === "unavailable");
|
|
3227
|
-
if (unavailableProviders.length > 0) {
|
|
3228
|
-
recommendations.push("\u{1F527} Configure credentials for unavailable providers to get complete visibility");
|
|
3229
|
-
recommendations.push("\u{1F50D} Use --discover-profiles to check for existing but unconfigured profiles");
|
|
3230
|
-
}
|
|
3231
|
-
if (summary.totalCost > 1e3) {
|
|
3232
|
-
recommendations.push("\u{1F4C8} Set up --monitor for real-time cost tracking across all providers");
|
|
3233
|
-
recommendations.push("\u{1F6A8} Configure --alert-threshold for multi-cloud budget management");
|
|
3234
|
-
}
|
|
3235
|
-
recommendations.push("\u{1F3F7}\uFE0F Implement consistent tagging strategy across all cloud providers");
|
|
3236
|
-
recommendations.push("\u{1F512} Use --dependency-mapping to understand cross-cloud resource relationships");
|
|
3237
|
-
recommendations.forEach((rec, index) => {
|
|
3238
|
-
output += import_chalk4.default.gray(`${index + 1}. ${rec}
|
|
3239
|
-
`);
|
|
3240
|
-
});
|
|
3241
|
-
output += "\n" + import_chalk4.default.bold.yellow("\u26A1 Quick Actions:") + "\n";
|
|
3242
|
-
output += import_chalk4.default.gray("\u2022 infra-cost --all-profiles --combine-profiles # Aggregate view\n");
|
|
3243
|
-
output += import_chalk4.default.gray("\u2022 infra-cost --compare-clouds aws,gcp,azure # Cost comparison\n");
|
|
3244
|
-
output += import_chalk4.default.gray("\u2022 infra-cost --inventory --group-by provider # Detailed inventory\n");
|
|
3245
|
-
return output;
|
|
3246
|
-
}
|
|
3247
|
-
// Helper methods
|
|
3248
|
-
createProviderAdapter(provider, profile) {
|
|
3249
|
-
try {
|
|
3250
|
-
const config = {
|
|
3251
|
-
provider,
|
|
3252
|
-
credentials: profile.credentials || {},
|
|
3253
|
-
region: profile.region,
|
|
3254
|
-
profile: profile.name
|
|
3255
|
-
};
|
|
3256
|
-
return this.factory.createProvider(config);
|
|
3257
|
-
} catch (error) {
|
|
3258
|
-
console.warn(`Failed to create provider adapter for ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
3259
|
-
return null;
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
getProviderDisplayName(provider) {
|
|
3263
|
-
const names = CloudProviderFactory.getProviderDisplayNames();
|
|
3264
|
-
return names[provider] || provider.toUpperCase();
|
|
3265
|
-
}
|
|
3266
|
-
getStatusIcon(status) {
|
|
3267
|
-
switch (status) {
|
|
3268
|
-
case "active":
|
|
3269
|
-
return "\u2705";
|
|
3270
|
-
case "unavailable":
|
|
3271
|
-
return "\u26AA";
|
|
3272
|
-
case "error":
|
|
3273
|
-
return "\u274C";
|
|
3274
|
-
default:
|
|
3275
|
-
return "\u26AA";
|
|
3276
|
-
}
|
|
3277
|
-
}
|
|
3278
|
-
getStatusColor(status) {
|
|
3279
|
-
switch (status) {
|
|
3280
|
-
case "active":
|
|
3281
|
-
return "green";
|
|
3282
|
-
case "unavailable":
|
|
3283
|
-
return "gray";
|
|
3284
|
-
case "error":
|
|
3285
|
-
return "red";
|
|
3286
|
-
default:
|
|
3287
|
-
return "gray";
|
|
3288
|
-
}
|
|
3289
|
-
}
|
|
3290
|
-
getActiveProvidersList(summary) {
|
|
3291
|
-
const active = Object.entries(summary.providerBreakdown).filter(([, data]) => data.status === "active").map(([provider]) => provider.toUpperCase());
|
|
3292
|
-
return active.join(", ") || "None";
|
|
3293
|
-
}
|
|
3294
|
-
getResourceTypeBreakdown(summary) {
|
|
3295
|
-
const top3 = Object.entries(summary.consolidatedResourcesByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a).slice(0, 3).map(([type, count]) => `${count} ${type}`);
|
|
3296
|
-
return top3.join(", ") || "None";
|
|
3297
|
-
}
|
|
3298
|
-
getCostBreakdownByProvider(summary) {
|
|
3299
|
-
const costs = Object.entries(summary.providerBreakdown).filter(([, data]) => data.status === "active" && data.cost > 0).map(([provider, data]) => `${provider}: $${data.cost.toFixed(0)}`);
|
|
3300
|
-
return costs.join(", ") || "None";
|
|
3301
|
-
}
|
|
3302
|
-
createResourceTypeRows(summary) {
|
|
3303
|
-
return Object.entries(summary.consolidatedResourcesByType).filter(([, count]) => count > 0).sort(([, a], [, b]) => b - a).map(([type, count]) => {
|
|
3304
|
-
const percentage = (count / summary.totalResources * 100).toFixed(1) + "%";
|
|
3305
|
-
const topProvider = this.getTopProviderForResourceType(summary, type);
|
|
3306
|
-
return {
|
|
3307
|
-
resourceType: type.charAt(0).toUpperCase() + type.slice(1),
|
|
3308
|
-
totalCount: count.toLocaleString(),
|
|
3309
|
-
percentage,
|
|
3310
|
-
topProvider: topProvider ? topProvider.toUpperCase() : "N/A"
|
|
3311
|
-
};
|
|
3312
|
-
});
|
|
3313
|
-
}
|
|
3314
|
-
getTopProviderForResourceType(summary, resourceType) {
|
|
3315
|
-
let maxCount = 0;
|
|
3316
|
-
let topProvider = null;
|
|
3317
|
-
Object.entries(summary.providerBreakdown).forEach(([provider, data]) => {
|
|
3318
|
-
if (data.inventory && data.inventory.resourcesByType[resourceType] > maxCount) {
|
|
3319
|
-
maxCount = data.inventory.resourcesByType[resourceType];
|
|
3320
|
-
topProvider = provider;
|
|
3321
|
-
}
|
|
3322
|
-
});
|
|
3323
|
-
return topProvider;
|
|
3324
|
-
}
|
|
3325
|
-
calculateTopResourcesByProvider(summary) {
|
|
3326
|
-
const results = [];
|
|
3327
|
-
Object.entries(summary.providerBreakdown).forEach(([provider, data]) => {
|
|
3328
|
-
if (data.inventory && data.resourceCount > 0) {
|
|
3329
|
-
Object.entries(data.inventory.resourcesByType).forEach(([type, count]) => {
|
|
3330
|
-
if (count > 0) {
|
|
3331
|
-
results.push({
|
|
3332
|
-
provider,
|
|
3333
|
-
resourceType: type,
|
|
3334
|
-
count,
|
|
3335
|
-
percentage: count / summary.totalResources * 100
|
|
3336
|
-
});
|
|
3337
|
-
}
|
|
3338
|
-
});
|
|
3339
|
-
}
|
|
3340
|
-
});
|
|
3341
|
-
return results.sort((a, b) => b.count - a.count).slice(0, 10);
|
|
3342
|
-
}
|
|
3343
|
-
};
|
|
3344
|
-
|
|
3345
|
-
// src/demo/test-multi-cloud-dashboard.ts
|
|
3346
|
-
var import_meta = {};
|
|
3347
|
-
async function testMultiCloudDashboard() {
|
|
3348
|
-
console.log("\u{1F680} Testing Multi-Cloud Dashboard...\n");
|
|
3349
|
-
try {
|
|
3350
|
-
const dashboard = new MultiCloudDashboard();
|
|
3351
|
-
console.log("\u{1F4CA} Test 1: Full Multi-Cloud Dashboard");
|
|
3352
|
-
console.log("\u2500".repeat(50));
|
|
3353
|
-
const fullDashboard = await dashboard.generateMultiCloudInventoryDashboard();
|
|
3354
|
-
console.log(fullDashboard);
|
|
3355
|
-
console.log("\n\u{1F4CA} Test 2: Specific Providers Dashboard (AWS + GCP)");
|
|
3356
|
-
console.log("\u2500".repeat(50));
|
|
3357
|
-
const specificProviders = await dashboard.generateMultiCloudInventoryDashboard([
|
|
3358
|
-
"aws" /* AWS */,
|
|
3359
|
-
"gcp" /* GOOGLE_CLOUD */
|
|
3360
|
-
]);
|
|
3361
|
-
console.log(specificProviders);
|
|
3362
|
-
console.log("\n\u2705 Multi-Cloud Dashboard tests completed!");
|
|
3363
|
-
console.log("\n\u{1F4A1} Usage Examples:");
|
|
3364
|
-
console.log(" infra-cost --multi-cloud-dashboard");
|
|
3365
|
-
console.log(" infra-cost --all-clouds-inventory");
|
|
3366
|
-
console.log(" infra-cost --multi-cloud-dashboard --compare-clouds aws,gcp,azure");
|
|
3367
|
-
console.log(" infra-cost --inventory --all-profiles");
|
|
3368
|
-
} catch (error) {
|
|
3369
|
-
console.error("\u274C Error testing multi-cloud dashboard:", error instanceof Error ? error.message : error);
|
|
3370
|
-
}
|
|
3371
|
-
}
|
|
3372
|
-
if (import_meta.url === `file://${process.argv[1]}`) {
|
|
3373
|
-
testMultiCloudDashboard();
|
|
3374
|
-
}
|
|
3375
|
-
//# sourceMappingURL=test-multi-cloud-dashboard.js.map
|