@jadenrazo/cloudcost-mcp 1.0.1 → 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/CHANGELOG.md +40 -5
- package/README.md +70 -115
- package/VERSIONING.md +99 -0
- package/data/aws-pricing/metadata.json +1 -1
- package/data/azure-pricing/metadata.json +1 -1
- package/dist/{chunk-6O2Y6MKU.js → chunk-CGMU4ETN.js} +927 -1067
- package/dist/{chunk-MNFT5YKN.js → chunk-LWTVE7UY.js} +19 -1
- package/dist/cli.d.ts +41 -0
- package/dist/cli.js +63 -22
- package/dist/index.js +361 -137
- package/dist/{loader-UWVEXYMR.js → loader-CMBV7346.js} +4 -2
- package/package.json +34 -21
- package/MIGRATION.md +0 -40
- package/STABILITY.md +0 -71
|
@@ -8,10 +8,11 @@ import {
|
|
|
8
8
|
getGcpSqlPricing,
|
|
9
9
|
getGcpStoragePricing,
|
|
10
10
|
getInstanceMap,
|
|
11
|
+
getPricingMetadata,
|
|
11
12
|
getRegionMappings,
|
|
12
13
|
getRegionPriceMultipliers,
|
|
13
14
|
getStorageMap
|
|
14
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-LWTVE7UY.js";
|
|
15
16
|
|
|
16
17
|
// src/config.ts
|
|
17
18
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -39,9 +40,39 @@ var DEFAULT_CONFIG = {
|
|
|
39
40
|
},
|
|
40
41
|
budget: {
|
|
41
42
|
warn_percentage: 80
|
|
43
|
+
},
|
|
44
|
+
guardrail: {
|
|
45
|
+
warn_ratio: 0.8
|
|
42
46
|
}
|
|
43
47
|
};
|
|
44
48
|
|
|
49
|
+
// src/logger.ts
|
|
50
|
+
var LEVEL_ORDER = {
|
|
51
|
+
debug: 0,
|
|
52
|
+
info: 1,
|
|
53
|
+
warn: 2,
|
|
54
|
+
error: 3
|
|
55
|
+
};
|
|
56
|
+
var currentLevel = "info";
|
|
57
|
+
function setLogLevel(level) {
|
|
58
|
+
currentLevel = level;
|
|
59
|
+
}
|
|
60
|
+
function shouldLog(level) {
|
|
61
|
+
return LEVEL_ORDER[level] >= LEVEL_ORDER[currentLevel];
|
|
62
|
+
}
|
|
63
|
+
function log(level, message, data) {
|
|
64
|
+
if (!shouldLog(level)) return;
|
|
65
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
66
|
+
const entry = data ? `[${timestamp}] ${level.toUpperCase()}: ${message} ${JSON.stringify(data)}` : `[${timestamp}] ${level.toUpperCase()}: ${message}`;
|
|
67
|
+
process.stderr.write(entry + "\n");
|
|
68
|
+
}
|
|
69
|
+
var logger = {
|
|
70
|
+
debug: (msg, data) => log("debug", msg, data),
|
|
71
|
+
info: (msg, data) => log("info", msg, data),
|
|
72
|
+
warn: (msg, data) => log("warn", msg, data),
|
|
73
|
+
error: (msg, data) => log("error", msg, data)
|
|
74
|
+
};
|
|
75
|
+
|
|
45
76
|
// src/config.ts
|
|
46
77
|
function getConfigDir() {
|
|
47
78
|
return join(homedir(), ".cloudcost");
|
|
@@ -51,7 +82,11 @@ function loadFileConfig() {
|
|
|
51
82
|
if (!existsSync(configPath)) return {};
|
|
52
83
|
try {
|
|
53
84
|
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
54
|
-
} catch {
|
|
85
|
+
} catch (err) {
|
|
86
|
+
logger.warn("Ignoring malformed config file; using defaults", {
|
|
87
|
+
path: configPath,
|
|
88
|
+
error: err instanceof Error ? err.message : String(err)
|
|
89
|
+
});
|
|
55
90
|
return {};
|
|
56
91
|
}
|
|
57
92
|
}
|
|
@@ -101,6 +136,23 @@ function loadEnvConfig() {
|
|
|
101
136
|
...warnPct !== void 0 && !isNaN(warnPct) && { warn_percentage: warnPct }
|
|
102
137
|
};
|
|
103
138
|
}
|
|
139
|
+
if (env.CLOUDCOST_GUARDRAIL_MAX_MONTHLY || env.CLOUDCOST_GUARDRAIL_MAX_PER_RESOURCE || env.CLOUDCOST_GUARDRAIL_WARN_RATIO) {
|
|
140
|
+
const parsePositive = (raw) => {
|
|
141
|
+
if (!raw) return void 0;
|
|
142
|
+
const v = parseFloat(raw);
|
|
143
|
+
return Number.isFinite(v) && v > 0 ? v : void 0;
|
|
144
|
+
};
|
|
145
|
+
const maxMonthly = parsePositive(env.CLOUDCOST_GUARDRAIL_MAX_MONTHLY);
|
|
146
|
+
const maxPerResource = parsePositive(env.CLOUDCOST_GUARDRAIL_MAX_PER_RESOURCE);
|
|
147
|
+
const warnRatioRaw = env.CLOUDCOST_GUARDRAIL_WARN_RATIO ? parseFloat(env.CLOUDCOST_GUARDRAIL_WARN_RATIO) : NaN;
|
|
148
|
+
const warnRatio = Number.isFinite(warnRatioRaw) && warnRatioRaw > 0 && warnRatioRaw <= 1 ? warnRatioRaw : void 0;
|
|
149
|
+
config.guardrail = {
|
|
150
|
+
...DEFAULT_CONFIG.guardrail,
|
|
151
|
+
...maxMonthly !== void 0 && { max_monthly: maxMonthly },
|
|
152
|
+
...maxPerResource !== void 0 && { max_per_resource: maxPerResource },
|
|
153
|
+
...warnRatio !== void 0 && { warn_ratio: warnRatio }
|
|
154
|
+
};
|
|
155
|
+
}
|
|
104
156
|
return config;
|
|
105
157
|
}
|
|
106
158
|
function loadConfig() {
|
|
@@ -133,43 +185,94 @@ function loadConfig() {
|
|
|
133
185
|
...DEFAULT_CONFIG.budget,
|
|
134
186
|
...fileConfig.budget,
|
|
135
187
|
...envConfig.budget
|
|
188
|
+
},
|
|
189
|
+
guardrail: {
|
|
190
|
+
...DEFAULT_CONFIG.guardrail,
|
|
191
|
+
...fileConfig.guardrail,
|
|
192
|
+
...envConfig.guardrail
|
|
136
193
|
}
|
|
137
194
|
};
|
|
138
195
|
}
|
|
139
196
|
|
|
140
|
-
// src/
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
197
|
+
// src/currency.ts
|
|
198
|
+
var SUPPORTED_CURRENCIES = [
|
|
199
|
+
"USD",
|
|
200
|
+
"EUR",
|
|
201
|
+
"GBP",
|
|
202
|
+
"JPY",
|
|
203
|
+
"CAD",
|
|
204
|
+
"AUD",
|
|
205
|
+
"INR",
|
|
206
|
+
"BRL"
|
|
207
|
+
];
|
|
208
|
+
var EXCHANGE_RATES_AS_OF = "2026-03";
|
|
209
|
+
var EXCHANGE_RATES = {
|
|
210
|
+
USD: 1,
|
|
211
|
+
EUR: 0.92,
|
|
212
|
+
GBP: 0.79,
|
|
213
|
+
JPY: 149.5,
|
|
214
|
+
CAD: 1.36,
|
|
215
|
+
AUD: 1.53,
|
|
216
|
+
INR: 83.1,
|
|
217
|
+
BRL: 4.97
|
|
151
218
|
};
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
219
|
+
function convertCurrency(amountUsd, targetCurrency) {
|
|
220
|
+
const rate = EXCHANGE_RATES[targetCurrency];
|
|
221
|
+
if (rate === void 0 || rate === 1) {
|
|
222
|
+
return amountUsd;
|
|
223
|
+
}
|
|
224
|
+
return amountUsd * rate;
|
|
155
225
|
}
|
|
156
|
-
function
|
|
157
|
-
return
|
|
226
|
+
function getExchangeRateInfo(currency) {
|
|
227
|
+
return {
|
|
228
|
+
rate: EXCHANGE_RATES[currency] ?? 1,
|
|
229
|
+
rate_as_of: EXCHANGE_RATES_AS_OF
|
|
230
|
+
};
|
|
158
231
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
232
|
+
function convertLineItem(item, currency) {
|
|
233
|
+
return {
|
|
234
|
+
...item,
|
|
235
|
+
unit_price: convertCurrency(item.unit_price, currency),
|
|
236
|
+
monthly_cost: convertCurrency(item.monthly_cost, currency)
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function convertCostEstimate(estimate, currency) {
|
|
240
|
+
return {
|
|
241
|
+
...estimate,
|
|
242
|
+
monthly_cost: convertCurrency(estimate.monthly_cost, currency),
|
|
243
|
+
yearly_cost: convertCurrency(estimate.yearly_cost, currency),
|
|
244
|
+
currency,
|
|
245
|
+
breakdown: estimate.breakdown.map((item) => convertLineItem(item, currency))
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function convertBreakdownCurrency(breakdown, currency) {
|
|
249
|
+
const original_usd = {
|
|
250
|
+
total_monthly: breakdown.total_monthly,
|
|
251
|
+
total_yearly: breakdown.total_yearly
|
|
252
|
+
};
|
|
253
|
+
const convertedByService = {};
|
|
254
|
+
for (const [service, amount] of Object.entries(breakdown.by_service)) {
|
|
255
|
+
convertedByService[service] = convertCurrency(amount, currency);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
...breakdown,
|
|
259
|
+
total_monthly: convertCurrency(breakdown.total_monthly, currency),
|
|
260
|
+
total_yearly: convertCurrency(breakdown.total_yearly, currency),
|
|
261
|
+
currency,
|
|
262
|
+
by_service: convertedByService,
|
|
263
|
+
by_resource: breakdown.by_resource.map((est) => convertCostEstimate(est, currency)),
|
|
264
|
+
...breakdown.estimated_egress_monthly !== void 0 && {
|
|
265
|
+
estimated_egress_monthly: convertCurrency(breakdown.estimated_egress_monthly, currency)
|
|
266
|
+
},
|
|
267
|
+
original_usd,
|
|
268
|
+
exchange_rate: getExchangeRateInfo(currency)
|
|
269
|
+
};
|
|
164
270
|
}
|
|
165
|
-
var logger = {
|
|
166
|
-
debug: (msg, data) => log("debug", msg, data),
|
|
167
|
-
info: (msg, data) => log("info", msg, data),
|
|
168
|
-
warn: (msg, data) => log("warn", msg, data),
|
|
169
|
-
error: (msg, data) => log("error", msg, data)
|
|
170
|
-
};
|
|
171
271
|
|
|
172
272
|
// src/pricing/cache.ts
|
|
273
|
+
import Database from "better-sqlite3";
|
|
274
|
+
import { mkdirSync } from "fs";
|
|
275
|
+
import { dirname } from "path";
|
|
173
276
|
var SCHEMA = `
|
|
174
277
|
CREATE TABLE IF NOT EXISTS pricing_cache (
|
|
175
278
|
key TEXT PRIMARY KEY,
|
|
@@ -525,6 +628,15 @@ var PricingCache = class {
|
|
|
525
628
|
}
|
|
526
629
|
};
|
|
527
630
|
|
|
631
|
+
// src/pricing/effective-date.ts
|
|
632
|
+
function resolveEffectiveDate(effectiveDate) {
|
|
633
|
+
if (effectiveDate) {
|
|
634
|
+
const t = new Date(effectiveDate);
|
|
635
|
+
if (!Number.isNaN(t.getTime())) return t.toISOString();
|
|
636
|
+
}
|
|
637
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
638
|
+
}
|
|
639
|
+
|
|
528
640
|
// src/pricing/aws/aws-normalizer.ts
|
|
529
641
|
function extractUsdPrice(priceDimensions) {
|
|
530
642
|
for (const dim of Object.values(priceDimensions)) {
|
|
@@ -562,94 +674,131 @@ function extractFromTerms(onDemandTerms, defaultUnit) {
|
|
|
562
674
|
}
|
|
563
675
|
return { price: 0, unit: defaultUnit };
|
|
564
676
|
}
|
|
565
|
-
function
|
|
566
|
-
if (effectiveDate) {
|
|
567
|
-
const t = new Date(effectiveDate);
|
|
568
|
-
if (!Number.isNaN(t.getTime())) return t.toISOString();
|
|
569
|
-
}
|
|
570
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
571
|
-
}
|
|
572
|
-
function normalizeAwsCompute(rawProduct, rawPrice, region, effectiveDate) {
|
|
573
|
-
const attrs = rawProduct?.attributes ?? {};
|
|
677
|
+
function normalize(rawPrice, region, effectiveDate, spec) {
|
|
574
678
|
const terms = rawPrice?.terms?.OnDemand ?? {};
|
|
575
|
-
const { price, unit } = extractFromTerms(terms,
|
|
679
|
+
const { price, unit } = extractFromTerms(terms, spec.defaultUnit);
|
|
576
680
|
return {
|
|
577
681
|
provider: "aws",
|
|
578
|
-
service:
|
|
579
|
-
resource_type:
|
|
682
|
+
service: spec.service,
|
|
683
|
+
resource_type: spec.resourceType,
|
|
580
684
|
region,
|
|
581
685
|
unit,
|
|
582
686
|
price_per_unit: price,
|
|
583
687
|
currency: "USD",
|
|
688
|
+
description: spec.description,
|
|
689
|
+
attributes: {
|
|
690
|
+
...spec.attributes,
|
|
691
|
+
pricing_source: "live"
|
|
692
|
+
},
|
|
693
|
+
effective_date: resolveEffectiveDate(effectiveDate)
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
function normalizeAwsCompute(rawProduct, rawPrice, region, effectiveDate) {
|
|
697
|
+
const attrs = rawProduct?.attributes ?? {};
|
|
698
|
+
return normalize(rawPrice, region, effectiveDate, {
|
|
699
|
+
service: "ec2",
|
|
700
|
+
defaultUnit: "Hrs",
|
|
701
|
+
resourceType: attrs.instanceType ?? "unknown",
|
|
584
702
|
description: attrs.instanceType ? `AWS EC2 ${attrs.instanceType} (${attrs.operatingSystem ?? "Linux"})` : void 0,
|
|
585
703
|
attributes: {
|
|
586
704
|
instance_type: attrs.instanceType ?? "",
|
|
587
705
|
vcpu: attrs.vcpu ?? "",
|
|
588
706
|
memory: attrs.memory ?? "",
|
|
589
707
|
operating_system: attrs.operatingSystem ?? "Linux",
|
|
590
|
-
tenancy: attrs.tenancy ?? "Shared"
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
effective_date: resolveEffectiveDate(effectiveDate)
|
|
594
|
-
};
|
|
708
|
+
tenancy: attrs.tenancy ?? "Shared"
|
|
709
|
+
}
|
|
710
|
+
});
|
|
595
711
|
}
|
|
596
712
|
function normalizeAwsDatabase(rawProduct, rawPrice, region, effectiveDate) {
|
|
597
713
|
const attrs = rawProduct?.attributes ?? {};
|
|
598
|
-
|
|
599
|
-
const { price, unit } = extractFromTerms(terms, "Hrs");
|
|
600
|
-
return {
|
|
601
|
-
provider: "aws",
|
|
714
|
+
return normalize(rawPrice, region, effectiveDate, {
|
|
602
715
|
service: "rds",
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
unit,
|
|
606
|
-
price_per_unit: price,
|
|
607
|
-
currency: "USD",
|
|
716
|
+
defaultUnit: "Hrs",
|
|
717
|
+
resourceType: attrs.instanceType ?? "unknown",
|
|
608
718
|
description: attrs.instanceType ? `AWS RDS ${attrs.instanceType} (${attrs.databaseEngine ?? "MySQL"})` : void 0,
|
|
609
719
|
attributes: {
|
|
610
720
|
instance_type: attrs.instanceType ?? "",
|
|
611
721
|
database_engine: attrs.databaseEngine ?? "",
|
|
612
722
|
deployment_option: attrs.deploymentOption ?? "Single-AZ",
|
|
613
723
|
vcpu: attrs.vcpu ?? "",
|
|
614
|
-
memory: attrs.memory ?? ""
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
effective_date: resolveEffectiveDate(effectiveDate)
|
|
618
|
-
};
|
|
724
|
+
memory: attrs.memory ?? ""
|
|
725
|
+
}
|
|
726
|
+
});
|
|
619
727
|
}
|
|
620
728
|
function normalizeAwsStorage(rawProduct, rawPrice, region, effectiveDate) {
|
|
621
729
|
const attrs = rawProduct?.attributes ?? {};
|
|
622
|
-
const terms = rawPrice?.terms?.OnDemand ?? {};
|
|
623
|
-
const { price, unit } = extractFromTerms(terms, "GB-Mo");
|
|
624
730
|
const volumeType = attrs.volumeApiName ?? attrs.volumeType ?? "gp3";
|
|
625
|
-
return {
|
|
626
|
-
provider: "aws",
|
|
731
|
+
return normalize(rawPrice, region, effectiveDate, {
|
|
627
732
|
service: "ebs",
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
unit,
|
|
631
|
-
price_per_unit: price,
|
|
632
|
-
currency: "USD",
|
|
733
|
+
defaultUnit: "GB-Mo",
|
|
734
|
+
resourceType: volumeType,
|
|
633
735
|
description: `AWS EBS ${volumeType} volume`,
|
|
634
736
|
attributes: {
|
|
635
737
|
volume_type: volumeType,
|
|
636
738
|
max_iops: attrs.maxIopsvolume ?? "",
|
|
637
|
-
max_throughput: attrs.maxThroughputvolume ?? ""
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
effective_date: resolveEffectiveDate(effectiveDate)
|
|
641
|
-
};
|
|
739
|
+
max_throughput: attrs.maxThroughputvolume ?? ""
|
|
740
|
+
}
|
|
741
|
+
});
|
|
642
742
|
}
|
|
643
743
|
|
|
644
744
|
// src/pricing/fetch-utils.ts
|
|
745
|
+
var DEFAULT_MAX_RESPONSE_BYTES = 100 * 1024 * 1024;
|
|
746
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
747
|
+
function assertUrlAllowed(url, options) {
|
|
748
|
+
let parsed;
|
|
749
|
+
try {
|
|
750
|
+
parsed = new URL(url);
|
|
751
|
+
} catch {
|
|
752
|
+
throw new Error(`fetchWithRetry: invalid URL`);
|
|
753
|
+
}
|
|
754
|
+
const requireHttps = options.requireHttps ?? true;
|
|
755
|
+
if (requireHttps && parsed.protocol !== "https:") {
|
|
756
|
+
throw new Error(`fetchWithRetry: rejected non-https URL (scheme=${parsed.protocol})`);
|
|
757
|
+
}
|
|
758
|
+
if (options.allowedHosts && options.allowedHosts.length > 0) {
|
|
759
|
+
if (!options.allowedHosts.includes(parsed.host)) {
|
|
760
|
+
throw new Error(`fetchWithRetry: host ${parsed.host} not in allowlist`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return parsed;
|
|
764
|
+
}
|
|
765
|
+
function capResponseBody(res, maxBytes) {
|
|
766
|
+
if (!res.body) return res;
|
|
767
|
+
if (!Number.isFinite(maxBytes)) return res;
|
|
768
|
+
let seen = 0;
|
|
769
|
+
const limiter = new TransformStream({
|
|
770
|
+
transform(chunk, controller) {
|
|
771
|
+
seen += chunk.byteLength;
|
|
772
|
+
if (seen > maxBytes) {
|
|
773
|
+
controller.error(new Error(`fetchWithRetry: response body exceeded ${maxBytes} bytes`));
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
controller.enqueue(chunk);
|
|
777
|
+
}
|
|
778
|
+
});
|
|
779
|
+
const limited = res.body.pipeThrough(limiter);
|
|
780
|
+
return new Response(limited, {
|
|
781
|
+
status: res.status,
|
|
782
|
+
statusText: res.statusText,
|
|
783
|
+
headers: res.headers
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
function withDefaultSignal(init, timeoutMs) {
|
|
787
|
+
if (init?.signal) return init;
|
|
788
|
+
return { ...init ?? {}, signal: AbortSignal.timeout(timeoutMs) };
|
|
789
|
+
}
|
|
645
790
|
async function fetchWithRetry(url, init, options = {}) {
|
|
646
791
|
const maxRetries = options.maxRetries ?? 3;
|
|
647
792
|
const baseDelay = options.baseDelay ?? 1e3;
|
|
648
793
|
const maxDelay = options.maxDelay ?? 1e4;
|
|
794
|
+
const maxResponseBytes = options.maxResponseBytes ?? DEFAULT_MAX_RESPONSE_BYTES;
|
|
795
|
+
const defaultTimeoutMs = options.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
796
|
+
assertUrlAllowed(url, options);
|
|
797
|
+
const safeInit = withDefaultSignal(init, defaultTimeoutMs);
|
|
649
798
|
let lastError;
|
|
650
799
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
651
800
|
try {
|
|
652
|
-
const res = await fetch(url,
|
|
801
|
+
const res = await fetch(url, safeInit);
|
|
653
802
|
if (res.status === 429 || res.status >= 500 && res.status < 600) {
|
|
654
803
|
if (attempt < maxRetries) {
|
|
655
804
|
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
@@ -663,7 +812,7 @@ async function fetchWithRetry(url, init, options = {}) {
|
|
|
663
812
|
continue;
|
|
664
813
|
}
|
|
665
814
|
}
|
|
666
|
-
return res;
|
|
815
|
+
return capResponseBody(res, maxResponseBytes);
|
|
667
816
|
} catch (err) {
|
|
668
817
|
lastError = err;
|
|
669
818
|
if (attempt < maxRetries) {
|
|
@@ -684,7 +833,8 @@ async function fetchWithRetry(url, init, options = {}) {
|
|
|
684
833
|
}
|
|
685
834
|
}
|
|
686
835
|
}
|
|
687
|
-
|
|
836
|
+
if (lastError instanceof Error) throw lastError;
|
|
837
|
+
throw new Error(`fetchWithRetry: all ${maxRetries} retries failed for ${url}`);
|
|
688
838
|
}
|
|
689
839
|
function sleep(ms) {
|
|
690
840
|
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
@@ -1022,6 +1172,24 @@ var NAT_PER_GB = 0.045;
|
|
|
1022
1172
|
var EKS_HOURLY = 0.1;
|
|
1023
1173
|
var CACHE_TTL = 86400;
|
|
1024
1174
|
var BULK_PRICING_BASE = "https://pricing.us-east-1.amazonaws.com/offers/v1.0/aws";
|
|
1175
|
+
var AWS_PRICING_HOST = "pricing.us-east-1.amazonaws.com";
|
|
1176
|
+
var AWS_REGION_RE = /^[a-z]{2}(?:-(?:gov|iso|isob))?-[a-z]+-\d+$/;
|
|
1177
|
+
var AWS_PRICING_SERVICES = /* @__PURE__ */ new Set([
|
|
1178
|
+
"AmazonEC2",
|
|
1179
|
+
"AmazonRDS",
|
|
1180
|
+
"AmazonElastiCache",
|
|
1181
|
+
"AmazonRedshift"
|
|
1182
|
+
]);
|
|
1183
|
+
function assertValidAwsRegion(region) {
|
|
1184
|
+
if (typeof region !== "string" || !AWS_REGION_RE.test(region)) {
|
|
1185
|
+
throw new Error(`invalid AWS region: ${JSON.stringify(region).slice(0, 64)}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
function assertValidAwsPricingService(service) {
|
|
1189
|
+
if (typeof service !== "string" || !AWS_PRICING_SERVICES.has(service)) {
|
|
1190
|
+
throw new Error(`invalid AWS pricing service: ${JSON.stringify(service).slice(0, 64)}`);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1025
1193
|
var SIZE_ORDER = [
|
|
1026
1194
|
"nano",
|
|
1027
1195
|
"micro",
|
|
@@ -1038,11 +1206,12 @@ var SIZE_ORDER = [
|
|
|
1038
1206
|
"32xlarge",
|
|
1039
1207
|
"48xlarge"
|
|
1040
1208
|
];
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1209
|
+
|
|
1210
|
+
// src/pricing/region-multiplier.ts
|
|
1211
|
+
function regionMultiplier(provider, region) {
|
|
1212
|
+
const mult = getRegionPriceMultipliers()[provider][region.toLowerCase()];
|
|
1044
1213
|
if (mult === void 0) {
|
|
1045
|
-
logger.warn("region-multiplier: unknown
|
|
1214
|
+
logger.warn("region-multiplier: unknown region, defaulting to 1.0x", { provider, region });
|
|
1046
1215
|
return 1;
|
|
1047
1216
|
}
|
|
1048
1217
|
return mult;
|
|
@@ -1051,13 +1220,16 @@ function regionMultiplier(region) {
|
|
|
1051
1220
|
// src/pricing/aws/bulk-loader.ts
|
|
1052
1221
|
var AwsBulkLoader = class {
|
|
1053
1222
|
cache;
|
|
1223
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
1224
|
+
ttlSeconds;
|
|
1054
1225
|
/**
|
|
1055
1226
|
* Deduplicates concurrent streaming CSV fetches for the same region.
|
|
1056
1227
|
* Keyed by region; value is the in-flight Promise<boolean>.
|
|
1057
1228
|
*/
|
|
1058
1229
|
ec2CsvFetchInProgress = /* @__PURE__ */ new Map();
|
|
1059
|
-
constructor(cache) {
|
|
1230
|
+
constructor(cache, ttlSeconds = CACHE_TTL) {
|
|
1060
1231
|
this.cache = cache;
|
|
1232
|
+
this.ttlSeconds = ttlSeconds;
|
|
1061
1233
|
}
|
|
1062
1234
|
// -------------------------------------------------------------------------
|
|
1063
1235
|
// Public API
|
|
@@ -1084,7 +1256,7 @@ var AwsBulkLoader = class {
|
|
|
1084
1256
|
if (bulk) {
|
|
1085
1257
|
const result = this.extractEc2Price(bulk, instanceType, region, os);
|
|
1086
1258
|
if (result) {
|
|
1087
|
-
this.cache.set(cacheKey, result, "aws", "ec2", region,
|
|
1259
|
+
this.cache.set(cacheKey, result, "aws", "ec2", region, this.ttlSeconds);
|
|
1088
1260
|
return result;
|
|
1089
1261
|
}
|
|
1090
1262
|
}
|
|
@@ -1106,7 +1278,7 @@ var AwsBulkLoader = class {
|
|
|
1106
1278
|
if (bulk) {
|
|
1107
1279
|
const result = this.extractRdsPrice(bulk, instanceClass, region, engine);
|
|
1108
1280
|
if (result) {
|
|
1109
|
-
this.cache.set(cacheKey, result, "aws", "rds", region,
|
|
1281
|
+
this.cache.set(cacheKey, result, "aws", "rds", region, this.ttlSeconds);
|
|
1110
1282
|
return result;
|
|
1111
1283
|
}
|
|
1112
1284
|
}
|
|
@@ -1128,7 +1300,7 @@ var AwsBulkLoader = class {
|
|
|
1128
1300
|
if (bulk) {
|
|
1129
1301
|
const result = this.extractEbsPrice(bulk, volumeType, region);
|
|
1130
1302
|
if (result) {
|
|
1131
|
-
this.cache.set(cacheKey, result, "aws", "ebs", region,
|
|
1303
|
+
this.cache.set(cacheKey, result, "aws", "ebs", region, this.ttlSeconds);
|
|
1132
1304
|
return result;
|
|
1133
1305
|
}
|
|
1134
1306
|
}
|
|
@@ -1141,9 +1313,9 @@ var AwsBulkLoader = class {
|
|
|
1141
1313
|
}
|
|
1142
1314
|
return this.fallbackStoragePrice(volumeType, region, cacheKey);
|
|
1143
1315
|
}
|
|
1144
|
-
|
|
1145
|
-
const multiplier = regionMultiplier(region);
|
|
1146
|
-
return {
|
|
1316
|
+
getLoadBalancerPrice(region) {
|
|
1317
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1318
|
+
return Promise.resolve({
|
|
1147
1319
|
provider: "aws",
|
|
1148
1320
|
service: "elb",
|
|
1149
1321
|
resource_type: "alb",
|
|
@@ -1157,11 +1329,11 @@ var AwsBulkLoader = class {
|
|
|
1157
1329
|
pricing_model: "fixed_plus_lcu"
|
|
1158
1330
|
},
|
|
1159
1331
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1160
|
-
};
|
|
1332
|
+
});
|
|
1161
1333
|
}
|
|
1162
|
-
|
|
1163
|
-
const multiplier = regionMultiplier(region);
|
|
1164
|
-
return {
|
|
1334
|
+
getNatGatewayPrice(region) {
|
|
1335
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1336
|
+
return Promise.resolve({
|
|
1165
1337
|
provider: "aws",
|
|
1166
1338
|
service: "vpc",
|
|
1167
1339
|
resource_type: "nat-gateway",
|
|
@@ -1174,11 +1346,11 @@ var AwsBulkLoader = class {
|
|
|
1174
1346
|
per_gb_price: String(NAT_PER_GB * multiplier)
|
|
1175
1347
|
},
|
|
1176
1348
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1177
|
-
};
|
|
1349
|
+
});
|
|
1178
1350
|
}
|
|
1179
|
-
|
|
1180
|
-
const multiplier = regionMultiplier(region);
|
|
1181
|
-
return {
|
|
1351
|
+
getKubernetesPrice(region) {
|
|
1352
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1353
|
+
return Promise.resolve({
|
|
1182
1354
|
provider: "aws",
|
|
1183
1355
|
service: "eks",
|
|
1184
1356
|
resource_type: "cluster",
|
|
@@ -1189,7 +1361,7 @@ var AwsBulkLoader = class {
|
|
|
1189
1361
|
description: "AWS EKS control plane (per cluster/hour)",
|
|
1190
1362
|
attributes: {},
|
|
1191
1363
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1192
|
-
};
|
|
1364
|
+
});
|
|
1193
1365
|
}
|
|
1194
1366
|
// -------------------------------------------------------------------------
|
|
1195
1367
|
// Internal helpers – EC2 CSV streaming
|
|
@@ -1219,7 +1391,8 @@ var AwsBulkLoader = class {
|
|
|
1219
1391
|
return promise;
|
|
1220
1392
|
}
|
|
1221
1393
|
async _doFetchAndCacheEc2CsvPrices(region) {
|
|
1222
|
-
|
|
1394
|
+
assertValidAwsRegion(region);
|
|
1395
|
+
const url = `${BULK_PRICING_BASE}/AmazonEC2/current/${encodeURIComponent(region)}/index.csv`;
|
|
1223
1396
|
logger.debug("Streaming AWS EC2 CSV pricing", { url, region });
|
|
1224
1397
|
const controller = new AbortController();
|
|
1225
1398
|
const timeoutId = setTimeout(() => controller.abort(), 12e4);
|
|
@@ -1230,7 +1403,11 @@ var AwsBulkLoader = class {
|
|
|
1230
1403
|
{
|
|
1231
1404
|
maxRetries: 1,
|
|
1232
1405
|
baseDelay: 500,
|
|
1233
|
-
maxDelay: 2e3
|
|
1406
|
+
maxDelay: 2e3,
|
|
1407
|
+
allowedHosts: [AWS_PRICING_HOST],
|
|
1408
|
+
// AWS EC2 CSVs run ~267 MiB today; allow headroom for growth while
|
|
1409
|
+
// still hard-bounding at 512 MiB to prevent adversarial OOM.
|
|
1410
|
+
maxResponseBytes: 512 * 1024 * 1024
|
|
1234
1411
|
}
|
|
1235
1412
|
);
|
|
1236
1413
|
if (!res.ok) {
|
|
@@ -1351,7 +1528,7 @@ var AwsBulkLoader = class {
|
|
|
1351
1528
|
},
|
|
1352
1529
|
effective_date: effectiveDate
|
|
1353
1530
|
};
|
|
1354
|
-
this.cache.set(csvCacheKey, normalized, "aws", "ec2", region,
|
|
1531
|
+
this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, this.ttlSeconds);
|
|
1355
1532
|
cachedCount++;
|
|
1356
1533
|
}
|
|
1357
1534
|
}
|
|
@@ -1388,7 +1565,7 @@ var AwsBulkLoader = class {
|
|
|
1388
1565
|
"aws",
|
|
1389
1566
|
"ec2",
|
|
1390
1567
|
region,
|
|
1391
|
-
|
|
1568
|
+
this.ttlSeconds
|
|
1392
1569
|
);
|
|
1393
1570
|
cachedCount++;
|
|
1394
1571
|
}
|
|
@@ -1420,14 +1597,19 @@ var AwsBulkLoader = class {
|
|
|
1420
1597
|
// Internal helpers – live API fetch
|
|
1421
1598
|
// -------------------------------------------------------------------------
|
|
1422
1599
|
async fetchBulkPricing(service, region) {
|
|
1423
|
-
|
|
1600
|
+
assertValidAwsPricingService(service);
|
|
1601
|
+
assertValidAwsRegion(region);
|
|
1602
|
+
const url = `${BULK_PRICING_BASE}/${encodeURIComponent(service)}/current/${encodeURIComponent(region)}/index.json`;
|
|
1424
1603
|
logger.debug("Fetching AWS bulk pricing", { url });
|
|
1425
1604
|
const res = await fetchWithRetryAndCircuitBreaker(
|
|
1426
1605
|
url,
|
|
1427
1606
|
{
|
|
1428
1607
|
signal: AbortSignal.timeout(3e4)
|
|
1429
1608
|
},
|
|
1430
|
-
{
|
|
1609
|
+
{
|
|
1610
|
+
maxRetries: 0,
|
|
1611
|
+
allowedHosts: [AWS_PRICING_HOST]
|
|
1612
|
+
}
|
|
1431
1613
|
);
|
|
1432
1614
|
if (!res.ok) {
|
|
1433
1615
|
throw new Error(`HTTP ${res.status} from ${url}`);
|
|
@@ -1513,7 +1695,7 @@ var AwsBulkLoader = class {
|
|
|
1513
1695
|
logger.warn("No fallback price found for EC2 instance type", { instanceType });
|
|
1514
1696
|
return null;
|
|
1515
1697
|
}
|
|
1516
|
-
const multiplier = regionMultiplier(region);
|
|
1698
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1517
1699
|
const result = {
|
|
1518
1700
|
provider: "aws",
|
|
1519
1701
|
service: "ec2",
|
|
@@ -1531,7 +1713,7 @@ var AwsBulkLoader = class {
|
|
|
1531
1713
|
},
|
|
1532
1714
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1533
1715
|
};
|
|
1534
|
-
this.cache.set(cacheKey, result, "aws", "ec2", region,
|
|
1716
|
+
this.cache.set(cacheKey, result, "aws", "ec2", region, this.ttlSeconds);
|
|
1535
1717
|
return result;
|
|
1536
1718
|
}
|
|
1537
1719
|
fallbackDatabasePrice(instanceClass, region, engine, cacheKey) {
|
|
@@ -1540,7 +1722,7 @@ var AwsBulkLoader = class {
|
|
|
1540
1722
|
logger.warn("No fallback price found for RDS instance class", { instanceClass });
|
|
1541
1723
|
return null;
|
|
1542
1724
|
}
|
|
1543
|
-
const multiplier = regionMultiplier(region);
|
|
1725
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1544
1726
|
const result = {
|
|
1545
1727
|
provider: "aws",
|
|
1546
1728
|
service: "rds",
|
|
@@ -1558,7 +1740,7 @@ var AwsBulkLoader = class {
|
|
|
1558
1740
|
},
|
|
1559
1741
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1560
1742
|
};
|
|
1561
|
-
this.cache.set(cacheKey, result, "aws", "rds", region,
|
|
1743
|
+
this.cache.set(cacheKey, result, "aws", "rds", region, this.ttlSeconds);
|
|
1562
1744
|
return result;
|
|
1563
1745
|
}
|
|
1564
1746
|
fallbackStoragePrice(volumeType, region, cacheKey) {
|
|
@@ -1567,7 +1749,7 @@ var AwsBulkLoader = class {
|
|
|
1567
1749
|
logger.warn("No fallback price found for EBS volume type", { volumeType });
|
|
1568
1750
|
return null;
|
|
1569
1751
|
}
|
|
1570
|
-
const multiplier = regionMultiplier(region);
|
|
1752
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1571
1753
|
const result = {
|
|
1572
1754
|
provider: "aws",
|
|
1573
1755
|
service: "ebs",
|
|
@@ -1583,7 +1765,7 @@ var AwsBulkLoader = class {
|
|
|
1583
1765
|
},
|
|
1584
1766
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1585
1767
|
};
|
|
1586
|
-
this.cache.set(cacheKey, result, "aws", "ebs", region,
|
|
1768
|
+
this.cache.set(cacheKey, result, "aws", "ebs", region, this.ttlSeconds);
|
|
1587
1769
|
return result;
|
|
1588
1770
|
}
|
|
1589
1771
|
};
|
|
@@ -1593,10 +1775,13 @@ var SPOT_ADVISOR_URL = "https://website.spot.ec2.aws.a2z.com/spot.json";
|
|
|
1593
1775
|
var SPOT_ADVISOR_CACHE_KEY = "aws/spot-advisor/v1";
|
|
1594
1776
|
var AwsSpotClient = class {
|
|
1595
1777
|
cache;
|
|
1778
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
1779
|
+
ttlSeconds;
|
|
1596
1780
|
/** In-flight fetch deduplication. */
|
|
1597
1781
|
inflight = null;
|
|
1598
|
-
constructor(cache) {
|
|
1782
|
+
constructor(cache, ttlSeconds = CACHE_TTL) {
|
|
1599
1783
|
this.cache = cache;
|
|
1784
|
+
this.ttlSeconds = ttlSeconds;
|
|
1600
1785
|
}
|
|
1601
1786
|
/**
|
|
1602
1787
|
* Get the spot discount factor (0–1) for an instance type in a region.
|
|
@@ -1660,7 +1845,7 @@ var AwsSpotClient = class {
|
|
|
1660
1845
|
logger.debug("AWS Spot Advisor response missing spot_advisor key");
|
|
1661
1846
|
return null;
|
|
1662
1847
|
}
|
|
1663
|
-
this.cache.set(SPOT_ADVISOR_CACHE_KEY, doc, "aws", "spot-advisor", "global",
|
|
1848
|
+
this.cache.set(SPOT_ADVISOR_CACHE_KEY, doc, "aws", "spot-advisor", "global", this.ttlSeconds);
|
|
1664
1849
|
return doc;
|
|
1665
1850
|
} catch (err) {
|
|
1666
1851
|
logger.debug("AWS Spot Advisor fetch failed", {
|
|
@@ -1710,8 +1895,11 @@ function extractRiRatesFromBulk(bulk, instanceType, os = "Linux") {
|
|
|
1710
1895
|
}
|
|
1711
1896
|
var AwsReservedClient = class {
|
|
1712
1897
|
cache;
|
|
1713
|
-
|
|
1898
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
1899
|
+
ttlSeconds;
|
|
1900
|
+
constructor(cache, ttlSeconds = CACHE_TTL) {
|
|
1714
1901
|
this.cache = cache;
|
|
1902
|
+
this.ttlSeconds = ttlSeconds;
|
|
1715
1903
|
}
|
|
1716
1904
|
async getRiRates(service, instanceType, region, os = "Linux") {
|
|
1717
1905
|
if (service === "AmazonEC2") {
|
|
@@ -1727,13 +1915,18 @@ var AwsReservedClient = class {
|
|
|
1727
1915
|
const cacheKey = `aws/ri/${service.toLowerCase()}/${region.toLowerCase()}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
|
|
1728
1916
|
const cached = this.cache.get(cacheKey);
|
|
1729
1917
|
if (cached && Array.isArray(cached.rates)) return cached.rates;
|
|
1730
|
-
|
|
1918
|
+
assertValidAwsPricingService(service);
|
|
1919
|
+
assertValidAwsRegion(region);
|
|
1920
|
+
const url = `${BULK_PRICING_BASE}/${encodeURIComponent(service)}/current/${encodeURIComponent(region)}/index.json`;
|
|
1731
1921
|
logger.debug("Fetching AWS bulk pricing for RI rates", { url });
|
|
1732
1922
|
try {
|
|
1733
1923
|
const res = await fetchWithRetryAndCircuitBreaker(
|
|
1734
1924
|
url,
|
|
1735
1925
|
{ signal: AbortSignal.timeout(6e4) },
|
|
1736
|
-
{
|
|
1926
|
+
{
|
|
1927
|
+
maxRetries: 0,
|
|
1928
|
+
allowedHosts: [AWS_PRICING_HOST]
|
|
1929
|
+
}
|
|
1737
1930
|
);
|
|
1738
1931
|
if (!res.ok) {
|
|
1739
1932
|
logger.debug("AWS RI bulk fetch non-OK", { status: res.status });
|
|
@@ -1747,7 +1940,7 @@ var AwsReservedClient = class {
|
|
|
1747
1940
|
unit: "none",
|
|
1748
1941
|
rates
|
|
1749
1942
|
};
|
|
1750
|
-
this.cache.set(cacheKey, entry, "aws", "ri", region,
|
|
1943
|
+
this.cache.set(cacheKey, entry, "aws", "ri", region, this.ttlSeconds);
|
|
1751
1944
|
return rates;
|
|
1752
1945
|
} catch (err) {
|
|
1753
1946
|
logger.debug("AWS RI bulk fetch failed, will use static fallback", {
|
|
@@ -1809,67 +2002,48 @@ function normalizePayment(purchase) {
|
|
|
1809
2002
|
}
|
|
1810
2003
|
|
|
1811
2004
|
// src/pricing/azure/azure-normalizer.ts
|
|
1812
|
-
function
|
|
2005
|
+
function normalize2(item, spec) {
|
|
1813
2006
|
return {
|
|
1814
2007
|
provider: "azure",
|
|
1815
|
-
service:
|
|
2008
|
+
service: spec.service,
|
|
1816
2009
|
resource_type: item.skuName ?? item.armSkuName ?? "unknown",
|
|
1817
2010
|
region: item.armRegionName ?? item.location ?? "",
|
|
1818
|
-
unit: item.unitOfMeasure ??
|
|
2011
|
+
unit: item.unitOfMeasure ?? spec.defaultUnit,
|
|
1819
2012
|
price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
|
|
1820
2013
|
currency: item.currencyCode ?? "USD",
|
|
1821
|
-
tier: item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
|
|
2014
|
+
tier: spec.includeTier && item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
|
|
1822
2015
|
description: item.productName ?? void 0,
|
|
1823
2016
|
attributes: {
|
|
1824
2017
|
sku_name: item.skuName ?? "",
|
|
1825
|
-
|
|
1826
|
-
service_name: item.serviceName ??
|
|
2018
|
+
...spec.extraAttributes?.(item),
|
|
2019
|
+
service_name: item.serviceName ?? spec.defaultServiceName ?? "",
|
|
1827
2020
|
product_name: item.productName ?? "",
|
|
1828
2021
|
meter_name: item.meterName ?? "",
|
|
1829
2022
|
pricing_source: "live"
|
|
1830
2023
|
},
|
|
1831
|
-
effective_date: item.effectiveStartDate
|
|
2024
|
+
effective_date: resolveEffectiveDate(item.effectiveStartDate)
|
|
1832
2025
|
};
|
|
1833
2026
|
}
|
|
2027
|
+
function normalizeAzureCompute(item) {
|
|
2028
|
+
return normalize2(item, {
|
|
2029
|
+
service: "virtual-machines",
|
|
2030
|
+
defaultUnit: "1 Hour",
|
|
2031
|
+
includeTier: true,
|
|
2032
|
+
defaultServiceName: "Virtual Machines",
|
|
2033
|
+
extraAttributes: (i) => ({ arm_sku_name: i.armSkuName ?? "" })
|
|
2034
|
+
});
|
|
2035
|
+
}
|
|
1834
2036
|
function normalizeAzureDatabase(item) {
|
|
1835
|
-
return {
|
|
1836
|
-
provider: "azure",
|
|
2037
|
+
return normalize2(item, {
|
|
1837
2038
|
service: "azure-database",
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
unit: item.unitOfMeasure ?? "1 Hour",
|
|
1841
|
-
price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
|
|
1842
|
-
currency: item.currencyCode ?? "USD",
|
|
1843
|
-
description: item.productName ?? void 0,
|
|
1844
|
-
attributes: {
|
|
1845
|
-
sku_name: item.skuName ?? "",
|
|
1846
|
-
service_name: item.serviceName ?? "",
|
|
1847
|
-
product_name: item.productName ?? "",
|
|
1848
|
-
meter_name: item.meterName ?? "",
|
|
1849
|
-
pricing_source: "live"
|
|
1850
|
-
},
|
|
1851
|
-
effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1852
|
-
};
|
|
2039
|
+
defaultUnit: "1 Hour"
|
|
2040
|
+
});
|
|
1853
2041
|
}
|
|
1854
2042
|
function normalizeAzureStorage(item) {
|
|
1855
|
-
return {
|
|
1856
|
-
provider: "azure",
|
|
2043
|
+
return normalize2(item, {
|
|
1857
2044
|
service: "managed-disks",
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
unit: item.unitOfMeasure ?? "1 GiB/Month",
|
|
1861
|
-
price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
|
|
1862
|
-
currency: item.currencyCode ?? "USD",
|
|
1863
|
-
description: item.productName ?? void 0,
|
|
1864
|
-
attributes: {
|
|
1865
|
-
sku_name: item.skuName ?? "",
|
|
1866
|
-
service_name: item.serviceName ?? "",
|
|
1867
|
-
product_name: item.productName ?? "",
|
|
1868
|
-
meter_name: item.meterName ?? "",
|
|
1869
|
-
pricing_source: "live"
|
|
1870
|
-
},
|
|
1871
|
-
effective_date: item.effectiveStartDate ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1872
|
-
};
|
|
2045
|
+
defaultUnit: "1 GiB/Month"
|
|
2046
|
+
});
|
|
1873
2047
|
}
|
|
1874
2048
|
|
|
1875
2049
|
// src/pricing/azure/arm-regions.ts
|
|
@@ -2081,21 +2255,15 @@ var NAT_PER_GB2 = 0.045;
|
|
|
2081
2255
|
var AKS_HOURLY = 0.1;
|
|
2082
2256
|
var CACHE_TTL2 = 86400;
|
|
2083
2257
|
var RETAIL_API_BASE = "https://prices.azure.com/api/retail/prices";
|
|
2084
|
-
function regionMultiplier2(region) {
|
|
2085
|
-
const multipliers = getRegionPriceMultipliers();
|
|
2086
|
-
const mult = multipliers.azure[region.toLowerCase()];
|
|
2087
|
-
if (mult === void 0) {
|
|
2088
|
-
logger.warn("region-multiplier: unknown Azure region, defaulting to 1.0x", { region });
|
|
2089
|
-
return 1;
|
|
2090
|
-
}
|
|
2091
|
-
return mult;
|
|
2092
|
-
}
|
|
2093
2258
|
|
|
2094
2259
|
// src/pricing/azure/retail-client.ts
|
|
2095
2260
|
var AzureRetailClient = class {
|
|
2096
2261
|
cache;
|
|
2097
|
-
|
|
2262
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
2263
|
+
ttlSeconds;
|
|
2264
|
+
constructor(cache, ttlSeconds = CACHE_TTL2) {
|
|
2098
2265
|
this.cache = cache;
|
|
2266
|
+
this.ttlSeconds = ttlSeconds;
|
|
2099
2267
|
}
|
|
2100
2268
|
// -------------------------------------------------------------------------
|
|
2101
2269
|
// Public API
|
|
@@ -2111,7 +2279,7 @@ var AzureRetailClient = class {
|
|
|
2111
2279
|
const match = this.pickVmItem(items, vmSize, os);
|
|
2112
2280
|
if (match) {
|
|
2113
2281
|
const result = normalizeAzureCompute(match);
|
|
2114
|
-
this.cache.set(cacheKey, result, "azure", "vm", region,
|
|
2282
|
+
this.cache.set(cacheKey, result, "azure", "vm", region, this.ttlSeconds);
|
|
2115
2283
|
return result;
|
|
2116
2284
|
}
|
|
2117
2285
|
} catch (err) {
|
|
@@ -2132,7 +2300,7 @@ var AzureRetailClient = class {
|
|
|
2132
2300
|
const serviceName = `Azure Database for ${engine}`;
|
|
2133
2301
|
const result = await this.queryDatabasePrice(tier, armRegion, serviceName);
|
|
2134
2302
|
if (result) {
|
|
2135
|
-
this.cache.set(cacheKey, result, "azure", "db", region,
|
|
2303
|
+
this.cache.set(cacheKey, result, "azure", "db", region, this.ttlSeconds);
|
|
2136
2304
|
return result;
|
|
2137
2305
|
}
|
|
2138
2306
|
logger.warn("Azure Retail API returned no matching DB pricing", {
|
|
@@ -2214,7 +2382,7 @@ var AzureRetailClient = class {
|
|
|
2214
2382
|
if (items.length > 0) {
|
|
2215
2383
|
items.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
2216
2384
|
const result = normalizeAzureStorage(items[0]);
|
|
2217
|
-
this.cache.set(cacheKey, result, "azure", "storage", region,
|
|
2385
|
+
this.cache.set(cacheKey, result, "azure", "storage", region, this.ttlSeconds);
|
|
2218
2386
|
return result;
|
|
2219
2387
|
}
|
|
2220
2388
|
logger.warn("Azure Retail API returned no matching storage pricing", {
|
|
@@ -2230,9 +2398,9 @@ var AzureRetailClient = class {
|
|
|
2230
2398
|
}
|
|
2231
2399
|
return this.fallbackStoragePrice(diskType, region, cacheKey);
|
|
2232
2400
|
}
|
|
2233
|
-
|
|
2234
|
-
const multiplier =
|
|
2235
|
-
return {
|
|
2401
|
+
getLoadBalancerPrice(region) {
|
|
2402
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2403
|
+
return Promise.resolve({
|
|
2236
2404
|
provider: "azure",
|
|
2237
2405
|
service: "load-balancer",
|
|
2238
2406
|
resource_type: "standard",
|
|
@@ -2246,11 +2414,11 @@ var AzureRetailClient = class {
|
|
|
2246
2414
|
pricing_model: "fixed_plus_rules"
|
|
2247
2415
|
},
|
|
2248
2416
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2249
|
-
};
|
|
2417
|
+
});
|
|
2250
2418
|
}
|
|
2251
|
-
|
|
2252
|
-
const multiplier =
|
|
2253
|
-
return {
|
|
2419
|
+
getNatGatewayPrice(region) {
|
|
2420
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2421
|
+
return Promise.resolve({
|
|
2254
2422
|
provider: "azure",
|
|
2255
2423
|
service: "nat-gateway",
|
|
2256
2424
|
resource_type: "nat-gateway",
|
|
@@ -2263,11 +2431,11 @@ var AzureRetailClient = class {
|
|
|
2263
2431
|
per_gb_price: String(NAT_PER_GB2 * multiplier)
|
|
2264
2432
|
},
|
|
2265
2433
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2266
|
-
};
|
|
2434
|
+
});
|
|
2267
2435
|
}
|
|
2268
|
-
|
|
2269
|
-
const multiplier =
|
|
2270
|
-
return {
|
|
2436
|
+
getKubernetesPrice(region) {
|
|
2437
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2438
|
+
return Promise.resolve({
|
|
2271
2439
|
provider: "azure",
|
|
2272
2440
|
service: "aks",
|
|
2273
2441
|
resource_type: "cluster",
|
|
@@ -2278,7 +2446,7 @@ var AzureRetailClient = class {
|
|
|
2278
2446
|
description: "Azure Kubernetes Service (uptime SLA tier, per cluster/hour)",
|
|
2279
2447
|
attributes: {},
|
|
2280
2448
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2281
|
-
};
|
|
2449
|
+
});
|
|
2282
2450
|
}
|
|
2283
2451
|
/**
|
|
2284
2452
|
* Fetch the live Spot VM price for a given VM size/region/OS from the Azure
|
|
@@ -2310,7 +2478,7 @@ var AzureRetailClient = class {
|
|
|
2310
2478
|
const result = normalizeAzureCompute(spotItems[0]);
|
|
2311
2479
|
result.attributes.pricing_source = "live";
|
|
2312
2480
|
result.attributes.purchase_option = "spot";
|
|
2313
|
-
this.cache.set(cacheKey, result, "azure", "vm-spot", region,
|
|
2481
|
+
this.cache.set(cacheKey, result, "azure", "vm-spot", region, this.ttlSeconds);
|
|
2314
2482
|
return result;
|
|
2315
2483
|
}
|
|
2316
2484
|
} catch (err) {
|
|
@@ -2351,7 +2519,7 @@ var AzureRetailClient = class {
|
|
|
2351
2519
|
result.attributes.pricing_source = "live";
|
|
2352
2520
|
result.attributes.purchase_option = "reservation";
|
|
2353
2521
|
result.attributes.reservation_term = termLabel;
|
|
2354
|
-
this.cache.set(cacheKey, result, "azure", "vm-ri", region,
|
|
2522
|
+
this.cache.set(cacheKey, result, "azure", "vm-ri", region, this.ttlSeconds);
|
|
2355
2523
|
return result;
|
|
2356
2524
|
}
|
|
2357
2525
|
} catch (err) {
|
|
@@ -2432,16 +2600,20 @@ var AzureRetailClient = class {
|
|
|
2432
2600
|
pickVmItem(items, vmSize, os) {
|
|
2433
2601
|
if (items.length === 0) return null;
|
|
2434
2602
|
const sorted = [...items].sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
2435
|
-
const
|
|
2603
|
+
const skuFragment = vmSize.toLowerCase().replace(/^standard_/, "").replace(/_/g, " ");
|
|
2436
2604
|
const isWindows = os.toLowerCase().includes("windows");
|
|
2437
|
-
|
|
2605
|
+
const onDemand = sorted.filter((item) => {
|
|
2606
|
+
const meterLower = (item.meterName ?? "").toLowerCase();
|
|
2607
|
+
return !meterLower.includes("low priority") && !meterLower.includes("spot");
|
|
2608
|
+
});
|
|
2609
|
+
for (const item of onDemand) {
|
|
2438
2610
|
const skuLower = (item.skuName ?? "").toLowerCase();
|
|
2439
|
-
const hasWindows = skuLower.includes("windows");
|
|
2440
|
-
if (skuLower.includes(
|
|
2611
|
+
const hasWindows = skuLower.includes("windows") || (item.productName ?? "").toLowerCase().includes("windows");
|
|
2612
|
+
if (skuLower.includes(skuFragment) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
|
|
2441
2613
|
return item;
|
|
2442
2614
|
}
|
|
2443
2615
|
}
|
|
2444
|
-
return sorted[0];
|
|
2616
|
+
return onDemand[0] ?? sorted[0];
|
|
2445
2617
|
}
|
|
2446
2618
|
// -------------------------------------------------------------------------
|
|
2447
2619
|
// Internal helpers – size interpolation
|
|
@@ -2481,7 +2653,7 @@ var AzureRetailClient = class {
|
|
|
2481
2653
|
logger.warn("No fallback price found for Azure VM size", { vmSize });
|
|
2482
2654
|
return null;
|
|
2483
2655
|
}
|
|
2484
|
-
const multiplier =
|
|
2656
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2485
2657
|
const result = {
|
|
2486
2658
|
provider: "azure",
|
|
2487
2659
|
service: "virtual-machines",
|
|
@@ -2498,7 +2670,7 @@ var AzureRetailClient = class {
|
|
|
2498
2670
|
},
|
|
2499
2671
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2500
2672
|
};
|
|
2501
|
-
this.cache.set(cacheKey, result, "azure", "vm", region,
|
|
2673
|
+
this.cache.set(cacheKey, result, "azure", "vm", region, this.ttlSeconds);
|
|
2502
2674
|
return result;
|
|
2503
2675
|
}
|
|
2504
2676
|
fallbackDatabasePrice(tier, region, engine, cacheKey) {
|
|
@@ -2508,7 +2680,7 @@ var AzureRetailClient = class {
|
|
|
2508
2680
|
logger.warn("No fallback price found for Azure DB tier", { tier });
|
|
2509
2681
|
return null;
|
|
2510
2682
|
}
|
|
2511
|
-
const multiplier =
|
|
2683
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2512
2684
|
const result = {
|
|
2513
2685
|
provider: "azure",
|
|
2514
2686
|
service: "azure-database",
|
|
@@ -2525,7 +2697,7 @@ var AzureRetailClient = class {
|
|
|
2525
2697
|
},
|
|
2526
2698
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2527
2699
|
};
|
|
2528
|
-
this.cache.set(cacheKey, result, "azure", "db", region,
|
|
2700
|
+
this.cache.set(cacheKey, result, "azure", "db", region, this.ttlSeconds);
|
|
2529
2701
|
return result;
|
|
2530
2702
|
}
|
|
2531
2703
|
fallbackStoragePrice(diskType, region, cacheKey) {
|
|
@@ -2535,7 +2707,7 @@ var AzureRetailClient = class {
|
|
|
2535
2707
|
logger.warn("No fallback price found for Azure disk type", { diskType });
|
|
2536
2708
|
return null;
|
|
2537
2709
|
}
|
|
2538
|
-
const multiplier =
|
|
2710
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2539
2711
|
const result = {
|
|
2540
2712
|
provider: "azure",
|
|
2541
2713
|
service: "managed-disks",
|
|
@@ -2551,7 +2723,7 @@ var AzureRetailClient = class {
|
|
|
2551
2723
|
},
|
|
2552
2724
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2553
2725
|
};
|
|
2554
|
-
this.cache.set(cacheKey, result, "azure", "storage", region,
|
|
2726
|
+
this.cache.set(cacheKey, result, "azure", "storage", region, this.ttlSeconds);
|
|
2555
2727
|
return result;
|
|
2556
2728
|
}
|
|
2557
2729
|
};
|
|
@@ -2569,73 +2741,59 @@ function parseAzureVmSeries(vmName) {
|
|
|
2569
2741
|
}
|
|
2570
2742
|
|
|
2571
2743
|
// src/pricing/gcp/gcp-normalizer.ts
|
|
2572
|
-
function
|
|
2744
|
+
function normalize3(resourceType, pricePerUnit, region, spec, opts) {
|
|
2573
2745
|
return {
|
|
2574
2746
|
provider: "gcp",
|
|
2575
|
-
service:
|
|
2576
|
-
resource_type:
|
|
2747
|
+
service: spec.service,
|
|
2748
|
+
resource_type: resourceType,
|
|
2577
2749
|
region,
|
|
2578
|
-
unit:
|
|
2579
|
-
price_per_unit:
|
|
2750
|
+
unit: spec.unit,
|
|
2751
|
+
price_per_unit: pricePerUnit,
|
|
2580
2752
|
currency: "USD",
|
|
2581
|
-
description:
|
|
2753
|
+
description: `${spec.descriptionPrefix} ${resourceType}`,
|
|
2582
2754
|
attributes: {
|
|
2583
|
-
|
|
2584
|
-
|
|
2755
|
+
[spec.attrKey]: resourceType,
|
|
2756
|
+
...opts.skuId !== void 0 ? { sku_id: opts.skuId } : {},
|
|
2757
|
+
pricing_source: opts.source
|
|
2585
2758
|
},
|
|
2586
|
-
effective_date: (
|
|
2759
|
+
effective_date: resolveEffectiveDate(opts.effectiveDate)
|
|
2587
2760
|
};
|
|
2588
2761
|
}
|
|
2762
|
+
var COMPUTE_SPEC = {
|
|
2763
|
+
service: "compute-engine",
|
|
2764
|
+
unit: "h",
|
|
2765
|
+
descriptionPrefix: "GCP Compute Engine",
|
|
2766
|
+
attrKey: "machine_type"
|
|
2767
|
+
};
|
|
2768
|
+
var DATABASE_SPEC = {
|
|
2769
|
+
service: "cloud-sql",
|
|
2770
|
+
unit: "h",
|
|
2771
|
+
descriptionPrefix: "GCP Cloud SQL",
|
|
2772
|
+
attrKey: "tier"
|
|
2773
|
+
};
|
|
2774
|
+
var STORAGE_SPEC = {
|
|
2775
|
+
service: "cloud-storage",
|
|
2776
|
+
unit: "GiBy.mo",
|
|
2777
|
+
descriptionPrefix: "GCP Cloud Storage",
|
|
2778
|
+
attrKey: "storage_class"
|
|
2779
|
+
};
|
|
2780
|
+
var DISK_SPEC = {
|
|
2781
|
+
service: "persistent-disk",
|
|
2782
|
+
unit: "GiBy.mo",
|
|
2783
|
+
descriptionPrefix: "GCP Persistent Disk",
|
|
2784
|
+
attrKey: "disk_type"
|
|
2785
|
+
};
|
|
2786
|
+
function normalizeGcpCompute(machineType, hourlyPrice, region) {
|
|
2787
|
+
return normalize3(machineType, hourlyPrice, region, COMPUTE_SPEC, { source: "bundled" });
|
|
2788
|
+
}
|
|
2589
2789
|
function normalizeGcpDatabase(tier, hourlyPrice, region) {
|
|
2590
|
-
return {
|
|
2591
|
-
provider: "gcp",
|
|
2592
|
-
service: "cloud-sql",
|
|
2593
|
-
resource_type: tier,
|
|
2594
|
-
region,
|
|
2595
|
-
unit: "h",
|
|
2596
|
-
price_per_unit: hourlyPrice,
|
|
2597
|
-
currency: "USD",
|
|
2598
|
-
description: `GCP Cloud SQL ${tier}`,
|
|
2599
|
-
attributes: {
|
|
2600
|
-
tier,
|
|
2601
|
-
pricing_source: "bundled"
|
|
2602
|
-
},
|
|
2603
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2604
|
-
};
|
|
2790
|
+
return normalize3(tier, hourlyPrice, region, DATABASE_SPEC, { source: "bundled" });
|
|
2605
2791
|
}
|
|
2606
2792
|
function normalizeGcpStorage(storageClass, pricePerGb, region) {
|
|
2607
|
-
return {
|
|
2608
|
-
provider: "gcp",
|
|
2609
|
-
service: "cloud-storage",
|
|
2610
|
-
resource_type: storageClass,
|
|
2611
|
-
region,
|
|
2612
|
-
unit: "GiBy.mo",
|
|
2613
|
-
price_per_unit: pricePerGb,
|
|
2614
|
-
currency: "USD",
|
|
2615
|
-
description: `GCP Cloud Storage ${storageClass}`,
|
|
2616
|
-
attributes: {
|
|
2617
|
-
storage_class: storageClass,
|
|
2618
|
-
pricing_source: "bundled"
|
|
2619
|
-
},
|
|
2620
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2621
|
-
};
|
|
2793
|
+
return normalize3(storageClass, pricePerGb, region, STORAGE_SPEC, { source: "bundled" });
|
|
2622
2794
|
}
|
|
2623
2795
|
function normalizeGcpDisk(diskType, pricePerGb, region) {
|
|
2624
|
-
return {
|
|
2625
|
-
provider: "gcp",
|
|
2626
|
-
service: "persistent-disk",
|
|
2627
|
-
resource_type: diskType,
|
|
2628
|
-
region,
|
|
2629
|
-
unit: "GiBy.mo",
|
|
2630
|
-
price_per_unit: pricePerGb,
|
|
2631
|
-
currency: "USD",
|
|
2632
|
-
description: `GCP Persistent Disk ${diskType}`,
|
|
2633
|
-
attributes: {
|
|
2634
|
-
disk_type: diskType,
|
|
2635
|
-
pricing_source: "bundled"
|
|
2636
|
-
},
|
|
2637
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2638
|
-
};
|
|
2796
|
+
return normalize3(diskType, pricePerGb, region, DISK_SPEC, { source: "bundled" });
|
|
2639
2797
|
}
|
|
2640
2798
|
|
|
2641
2799
|
// src/pricing/gcp/bundled-loader.ts
|
|
@@ -2645,20 +2803,11 @@ var NAT_HOURLY3 = 0.044;
|
|
|
2645
2803
|
var NAT_PER_GB3 = 0.045;
|
|
2646
2804
|
var GKE_STANDARD_HOURLY = 0.1;
|
|
2647
2805
|
var GKE_AUTOPILOT_VCPU_HOURLY = 0.0445;
|
|
2648
|
-
function regionMultiplier3(region) {
|
|
2649
|
-
const multipliers = getRegionPriceMultipliers();
|
|
2650
|
-
const mult = multipliers.gcp[region.toLowerCase()];
|
|
2651
|
-
if (mult === void 0) {
|
|
2652
|
-
logger.warn("region-multiplier: unknown GCP region, defaulting to 1.0x", { region });
|
|
2653
|
-
return 1;
|
|
2654
|
-
}
|
|
2655
|
-
return mult;
|
|
2656
|
-
}
|
|
2657
2806
|
var GcpBundledLoader = class {
|
|
2658
2807
|
// -------------------------------------------------------------------------
|
|
2659
2808
|
// Public API
|
|
2660
2809
|
// -------------------------------------------------------------------------
|
|
2661
|
-
|
|
2810
|
+
getComputePrice(machineType, region) {
|
|
2662
2811
|
try {
|
|
2663
2812
|
const pricing = getGcpComputePricing();
|
|
2664
2813
|
const regionPrices = pricing[region];
|
|
@@ -2667,7 +2816,7 @@ var GcpBundledLoader = class {
|
|
|
2667
2816
|
region,
|
|
2668
2817
|
machineType
|
|
2669
2818
|
});
|
|
2670
|
-
return null;
|
|
2819
|
+
return Promise.resolve(null);
|
|
2671
2820
|
}
|
|
2672
2821
|
const price = regionPrices[machineType];
|
|
2673
2822
|
if (price === void 0) {
|
|
@@ -2675,19 +2824,19 @@ var GcpBundledLoader = class {
|
|
|
2675
2824
|
region,
|
|
2676
2825
|
machineType
|
|
2677
2826
|
});
|
|
2678
|
-
return null;
|
|
2827
|
+
return Promise.resolve(null);
|
|
2679
2828
|
}
|
|
2680
|
-
return normalizeGcpCompute(machineType, price, region);
|
|
2829
|
+
return Promise.resolve(normalizeGcpCompute(machineType, price, region));
|
|
2681
2830
|
} catch (err) {
|
|
2682
2831
|
logger.error("GCP compute pricing load error", {
|
|
2683
2832
|
machineType,
|
|
2684
2833
|
region,
|
|
2685
2834
|
err: err instanceof Error ? err.message : String(err)
|
|
2686
2835
|
});
|
|
2687
|
-
return null;
|
|
2836
|
+
return Promise.resolve(null);
|
|
2688
2837
|
}
|
|
2689
2838
|
}
|
|
2690
|
-
|
|
2839
|
+
getDatabasePrice(tier, region) {
|
|
2691
2840
|
try {
|
|
2692
2841
|
const pricing = getGcpSqlPricing();
|
|
2693
2842
|
const regionPrices = pricing[region];
|
|
@@ -2696,7 +2845,7 @@ var GcpBundledLoader = class {
|
|
|
2696
2845
|
region,
|
|
2697
2846
|
tier
|
|
2698
2847
|
});
|
|
2699
|
-
return null;
|
|
2848
|
+
return Promise.resolve(null);
|
|
2700
2849
|
}
|
|
2701
2850
|
const price = regionPrices[tier];
|
|
2702
2851
|
if (price === void 0 || tier === "storage_per_gb" || tier === "ha_multiplier") {
|
|
@@ -2704,19 +2853,19 @@ var GcpBundledLoader = class {
|
|
|
2704
2853
|
region,
|
|
2705
2854
|
tier
|
|
2706
2855
|
});
|
|
2707
|
-
return null;
|
|
2856
|
+
return Promise.resolve(null);
|
|
2708
2857
|
}
|
|
2709
|
-
return normalizeGcpDatabase(tier, price, region);
|
|
2858
|
+
return Promise.resolve(normalizeGcpDatabase(tier, price, region));
|
|
2710
2859
|
} catch (err) {
|
|
2711
2860
|
logger.error("GCP SQL pricing load error", {
|
|
2712
2861
|
tier,
|
|
2713
2862
|
region,
|
|
2714
2863
|
err: err instanceof Error ? err.message : String(err)
|
|
2715
2864
|
});
|
|
2716
|
-
return null;
|
|
2865
|
+
return Promise.resolve(null);
|
|
2717
2866
|
}
|
|
2718
2867
|
}
|
|
2719
|
-
|
|
2868
|
+
getStoragePrice(storageClass, region) {
|
|
2720
2869
|
try {
|
|
2721
2870
|
const pricing = getGcpStoragePricing();
|
|
2722
2871
|
const regionPrices = pricing[region];
|
|
@@ -2725,7 +2874,7 @@ var GcpBundledLoader = class {
|
|
|
2725
2874
|
region,
|
|
2726
2875
|
storageClass
|
|
2727
2876
|
});
|
|
2728
|
-
return null;
|
|
2877
|
+
return Promise.resolve(null);
|
|
2729
2878
|
}
|
|
2730
2879
|
const classKey = storageClass.toUpperCase();
|
|
2731
2880
|
const price = regionPrices[classKey];
|
|
@@ -2734,19 +2883,19 @@ var GcpBundledLoader = class {
|
|
|
2734
2883
|
region,
|
|
2735
2884
|
storageClass
|
|
2736
2885
|
});
|
|
2737
|
-
return null;
|
|
2886
|
+
return Promise.resolve(null);
|
|
2738
2887
|
}
|
|
2739
|
-
return normalizeGcpStorage(storageClass.toUpperCase(), price, region);
|
|
2888
|
+
return Promise.resolve(normalizeGcpStorage(storageClass.toUpperCase(), price, region));
|
|
2740
2889
|
} catch (err) {
|
|
2741
2890
|
logger.error("GCP Storage pricing load error", {
|
|
2742
2891
|
storageClass,
|
|
2743
2892
|
region,
|
|
2744
2893
|
err: err instanceof Error ? err.message : String(err)
|
|
2745
2894
|
});
|
|
2746
|
-
return null;
|
|
2895
|
+
return Promise.resolve(null);
|
|
2747
2896
|
}
|
|
2748
2897
|
}
|
|
2749
|
-
|
|
2898
|
+
getDiskPrice(diskType, region) {
|
|
2750
2899
|
try {
|
|
2751
2900
|
const pricing = getGcpDiskPricing();
|
|
2752
2901
|
const regionPrices = pricing[region];
|
|
@@ -2755,7 +2904,7 @@ var GcpBundledLoader = class {
|
|
|
2755
2904
|
region,
|
|
2756
2905
|
diskType
|
|
2757
2906
|
});
|
|
2758
|
-
return null;
|
|
2907
|
+
return Promise.resolve(null);
|
|
2759
2908
|
}
|
|
2760
2909
|
const diskKey = diskType;
|
|
2761
2910
|
const price = regionPrices[diskKey];
|
|
@@ -2764,21 +2913,21 @@ var GcpBundledLoader = class {
|
|
|
2764
2913
|
region,
|
|
2765
2914
|
diskType
|
|
2766
2915
|
});
|
|
2767
|
-
return null;
|
|
2916
|
+
return Promise.resolve(null);
|
|
2768
2917
|
}
|
|
2769
|
-
return normalizeGcpDisk(diskType, price, region);
|
|
2918
|
+
return Promise.resolve(normalizeGcpDisk(diskType, price, region));
|
|
2770
2919
|
} catch (err) {
|
|
2771
2920
|
logger.error("GCP Disk pricing load error", {
|
|
2772
2921
|
diskType,
|
|
2773
2922
|
region,
|
|
2774
2923
|
err: err instanceof Error ? err.message : String(err)
|
|
2775
2924
|
});
|
|
2776
|
-
return null;
|
|
2925
|
+
return Promise.resolve(null);
|
|
2777
2926
|
}
|
|
2778
2927
|
}
|
|
2779
|
-
|
|
2780
|
-
const multiplier =
|
|
2781
|
-
return {
|
|
2928
|
+
getLoadBalancerPrice(region) {
|
|
2929
|
+
const multiplier = regionMultiplier("gcp", region);
|
|
2930
|
+
return Promise.resolve({
|
|
2782
2931
|
provider: "gcp",
|
|
2783
2932
|
service: "cloud-load-balancing",
|
|
2784
2933
|
resource_type: "forwarding-rule",
|
|
@@ -2791,11 +2940,11 @@ var GcpBundledLoader = class {
|
|
|
2791
2940
|
per_gb_price: String(LB_PER_GB * multiplier)
|
|
2792
2941
|
},
|
|
2793
2942
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2794
|
-
};
|
|
2943
|
+
});
|
|
2795
2944
|
}
|
|
2796
|
-
|
|
2797
|
-
const multiplier =
|
|
2798
|
-
return {
|
|
2945
|
+
getNatGatewayPrice(region) {
|
|
2946
|
+
const multiplier = regionMultiplier("gcp", region);
|
|
2947
|
+
return Promise.resolve({
|
|
2799
2948
|
provider: "gcp",
|
|
2800
2949
|
service: "cloud-nat",
|
|
2801
2950
|
resource_type: "nat-gateway",
|
|
@@ -2808,12 +2957,12 @@ var GcpBundledLoader = class {
|
|
|
2808
2957
|
per_gb_price: String(NAT_PER_GB3 * multiplier)
|
|
2809
2958
|
},
|
|
2810
2959
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2811
|
-
};
|
|
2960
|
+
});
|
|
2812
2961
|
}
|
|
2813
|
-
|
|
2814
|
-
const multiplier =
|
|
2962
|
+
getKubernetesPrice(region, mode = "standard") {
|
|
2963
|
+
const multiplier = regionMultiplier("gcp", region);
|
|
2815
2964
|
const baseHourly = mode === "autopilot" ? GKE_AUTOPILOT_VCPU_HOURLY : GKE_STANDARD_HOURLY;
|
|
2816
|
-
return {
|
|
2965
|
+
return Promise.resolve({
|
|
2817
2966
|
provider: "gcp",
|
|
2818
2967
|
service: "gke",
|
|
2819
2968
|
resource_type: "cluster",
|
|
@@ -2830,7 +2979,7 @@ var GcpBundledLoader = class {
|
|
|
2830
2979
|
}
|
|
2831
2980
|
},
|
|
2832
2981
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2833
|
-
};
|
|
2982
|
+
});
|
|
2834
2983
|
}
|
|
2835
2984
|
};
|
|
2836
2985
|
|
|
@@ -2869,8 +3018,11 @@ function skuCoversRegion(sku, region) {
|
|
|
2869
3018
|
}
|
|
2870
3019
|
var CloudBillingClient = class {
|
|
2871
3020
|
cache;
|
|
2872
|
-
|
|
3021
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
3022
|
+
ttlSeconds;
|
|
3023
|
+
constructor(cache, ttlSeconds = CACHE_TTL3) {
|
|
2873
3024
|
this.cache = cache;
|
|
3025
|
+
this.ttlSeconds = ttlSeconds;
|
|
2874
3026
|
}
|
|
2875
3027
|
// -------------------------------------------------------------------------
|
|
2876
3028
|
// Public fetch methods
|
|
@@ -2888,7 +3040,7 @@ var CloudBillingClient = class {
|
|
|
2888
3040
|
const family = this.machineFamily(machineType);
|
|
2889
3041
|
const result = this.extractComputePrice(skus, machineType, family, region);
|
|
2890
3042
|
if (result) {
|
|
2891
|
-
this.cache.set(cacheKey, result, "gcp", "compute", region,
|
|
3043
|
+
this.cache.set(cacheKey, result, "gcp", "compute", region, this.ttlSeconds);
|
|
2892
3044
|
return result;
|
|
2893
3045
|
}
|
|
2894
3046
|
} catch (err) {
|
|
@@ -2911,7 +3063,7 @@ var CloudBillingClient = class {
|
|
|
2911
3063
|
const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_SQL, region);
|
|
2912
3064
|
const result = this.extractDatabasePrice(skus, tier, region);
|
|
2913
3065
|
if (result) {
|
|
2914
|
-
this.cache.set(cacheKey, result, "gcp", "cloud-sql", region,
|
|
3066
|
+
this.cache.set(cacheKey, result, "gcp", "cloud-sql", region, this.ttlSeconds);
|
|
2915
3067
|
return result;
|
|
2916
3068
|
}
|
|
2917
3069
|
} catch (err) {
|
|
@@ -2935,7 +3087,7 @@ var CloudBillingClient = class {
|
|
|
2935
3087
|
const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_STORAGE, region);
|
|
2936
3088
|
const result = this.extractStoragePrice(skus, storageClass, region);
|
|
2937
3089
|
if (result) {
|
|
2938
|
-
this.cache.set(cacheKey, result, "gcp", "cloud-storage", region,
|
|
3090
|
+
this.cache.set(cacheKey, result, "gcp", "cloud-storage", region, this.ttlSeconds);
|
|
2939
3091
|
return result;
|
|
2940
3092
|
}
|
|
2941
3093
|
} catch (err) {
|
|
@@ -2964,7 +3116,7 @@ var CloudBillingClient = class {
|
|
|
2964
3116
|
const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
|
|
2965
3117
|
const result = this.extractGpuPrice(skus, accelerator, region);
|
|
2966
3118
|
if (result) {
|
|
2967
|
-
this.cache.set(cacheKey, result, "gcp", "compute-gpu", region,
|
|
3119
|
+
this.cache.set(cacheKey, result, "gcp", "compute-gpu", region, this.ttlSeconds);
|
|
2968
3120
|
return result;
|
|
2969
3121
|
}
|
|
2970
3122
|
} catch (err) {
|
|
@@ -3046,7 +3198,7 @@ var CloudBillingClient = class {
|
|
|
3046
3198
|
if (result.term1yr === null && result.term3yr === null) {
|
|
3047
3199
|
return null;
|
|
3048
3200
|
}
|
|
3049
|
-
this.cache.set(cacheKey, result, "gcp", "compute-cud", region,
|
|
3201
|
+
this.cache.set(cacheKey, result, "gcp", "compute-cud", region, this.ttlSeconds);
|
|
3050
3202
|
return result;
|
|
3051
3203
|
} catch (err) {
|
|
3052
3204
|
logger.warn("GCP Cloud Billing CUD fetch failed", {
|
|
@@ -3071,7 +3223,7 @@ var CloudBillingClient = class {
|
|
|
3071
3223
|
const skus = await this.fetchSkus(SERVICE_IDS.GKE, region);
|
|
3072
3224
|
const result = this.extractAutopilotPodRates(skus, region);
|
|
3073
3225
|
if (result) {
|
|
3074
|
-
this.cache.set(cacheKey, result, "gcp", "gke-autopilot", region,
|
|
3226
|
+
this.cache.set(cacheKey, result, "gcp", "gke-autopilot", region, this.ttlSeconds);
|
|
3075
3227
|
return result;
|
|
3076
3228
|
}
|
|
3077
3229
|
} catch (err) {
|
|
@@ -3350,14 +3502,15 @@ var CloudBillingClient = class {
|
|
|
3350
3502
|
};
|
|
3351
3503
|
|
|
3352
3504
|
// src/pricing/pricing-engine.ts
|
|
3505
|
+
var DEFAULT_CACHE_TTL_SECONDS = 86400;
|
|
3353
3506
|
var AwsProvider = class {
|
|
3354
3507
|
loader;
|
|
3355
3508
|
spotClient;
|
|
3356
3509
|
reservedClient;
|
|
3357
|
-
constructor(cache) {
|
|
3358
|
-
this.loader = new AwsBulkLoader(cache);
|
|
3359
|
-
this.spotClient = new AwsSpotClient(cache);
|
|
3360
|
-
this.reservedClient = new AwsReservedClient(cache);
|
|
3510
|
+
constructor(cache, ttlSeconds) {
|
|
3511
|
+
this.loader = new AwsBulkLoader(cache, ttlSeconds);
|
|
3512
|
+
this.spotClient = new AwsSpotClient(cache, ttlSeconds);
|
|
3513
|
+
this.reservedClient = new AwsReservedClient(cache, ttlSeconds);
|
|
3361
3514
|
}
|
|
3362
3515
|
getSpotFactor(instanceType, region, os) {
|
|
3363
3516
|
return this.spotClient.getSpotFactor(
|
|
@@ -3390,8 +3543,8 @@ var AwsProvider = class {
|
|
|
3390
3543
|
};
|
|
3391
3544
|
var AzureProvider = class {
|
|
3392
3545
|
client;
|
|
3393
|
-
constructor(cache) {
|
|
3394
|
-
this.client = new AzureRetailClient(cache);
|
|
3546
|
+
constructor(cache, ttlSeconds) {
|
|
3547
|
+
this.client = new AzureRetailClient(cache, ttlSeconds);
|
|
3395
3548
|
}
|
|
3396
3549
|
getComputePrice(instanceType, region, os) {
|
|
3397
3550
|
return this.client.getComputePrice(instanceType, region, os);
|
|
@@ -3433,9 +3586,9 @@ var AzureProvider = class {
|
|
|
3433
3586
|
var GcpProvider = class {
|
|
3434
3587
|
loader;
|
|
3435
3588
|
liveClient;
|
|
3436
|
-
constructor(cache) {
|
|
3589
|
+
constructor(cache, ttlSeconds) {
|
|
3437
3590
|
this.loader = new GcpBundledLoader();
|
|
3438
|
-
this.liveClient = new CloudBillingClient(cache);
|
|
3591
|
+
this.liveClient = new CloudBillingClient(cache, ttlSeconds);
|
|
3439
3592
|
}
|
|
3440
3593
|
async getComputePrice(instanceType, region, _os) {
|
|
3441
3594
|
try {
|
|
@@ -3489,12 +3642,51 @@ var GcpProvider = class {
|
|
|
3489
3642
|
return this.loader.getKubernetesPrice(region, mode === "autopilot" ? "autopilot" : "standard");
|
|
3490
3643
|
}
|
|
3491
3644
|
};
|
|
3645
|
+
var SERVICE_ALIAS_LOOKUPS = (() => {
|
|
3646
|
+
const groups = [
|
|
3647
|
+
[
|
|
3648
|
+
["compute", "ec2", "vm", "instance", "virtual-machines"],
|
|
3649
|
+
(p, resourceType, region, attributes) => p.getComputePrice(resourceType, region, attributes.os)
|
|
3650
|
+
],
|
|
3651
|
+
[
|
|
3652
|
+
["database", "rds", "sql", "db", "cloud-sql", "azure-database"],
|
|
3653
|
+
(p, resourceType, region, attributes) => p.getDatabasePrice(resourceType, region, attributes.engine)
|
|
3654
|
+
],
|
|
3655
|
+
[
|
|
3656
|
+
["storage", "ebs", "disk", "gcs", "managed-disks", "persistent-disk", "cloud-storage"],
|
|
3657
|
+
(p, resourceType, region, attributes) => {
|
|
3658
|
+
const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
|
|
3659
|
+
return p.getStoragePrice(resourceType, region, sizeGb);
|
|
3660
|
+
}
|
|
3661
|
+
],
|
|
3662
|
+
[
|
|
3663
|
+
["lb", "load-balancer", "alb", "nlb", "elb", "cloud-load-balancing"],
|
|
3664
|
+
(p, resourceType, region) => p.getLoadBalancerPrice(resourceType, region)
|
|
3665
|
+
],
|
|
3666
|
+
[
|
|
3667
|
+
["nat", "nat-gateway", "cloud-nat"],
|
|
3668
|
+
(p, _resourceType, region) => p.getNatGatewayPrice(region)
|
|
3669
|
+
],
|
|
3670
|
+
[
|
|
3671
|
+
["k8s", "kubernetes", "eks", "aks", "gke"],
|
|
3672
|
+
(p, _resourceType, region, attributes) => p.getKubernetesPrice(region, attributes.mode)
|
|
3673
|
+
]
|
|
3674
|
+
];
|
|
3675
|
+
const map = /* @__PURE__ */ new Map();
|
|
3676
|
+
for (const [aliases, lookup] of groups) {
|
|
3677
|
+
for (const alias of aliases) map.set(alias, lookup);
|
|
3678
|
+
}
|
|
3679
|
+
return map;
|
|
3680
|
+
})();
|
|
3492
3681
|
var PricingEngine = class {
|
|
3493
3682
|
providers = /* @__PURE__ */ new Map();
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
this.
|
|
3497
|
-
|
|
3683
|
+
config;
|
|
3684
|
+
constructor(cache, config) {
|
|
3685
|
+
this.config = config;
|
|
3686
|
+
const ttlSeconds = config?.cache?.ttl_seconds ?? DEFAULT_CACHE_TTL_SECONDS;
|
|
3687
|
+
this.providers.set("aws", new AwsProvider(cache, ttlSeconds));
|
|
3688
|
+
this.providers.set("azure", new AzureProvider(cache, ttlSeconds));
|
|
3689
|
+
this.providers.set("gcp", new GcpProvider(cache, ttlSeconds));
|
|
3498
3690
|
}
|
|
3499
3691
|
/**
|
|
3500
3692
|
* Return the PricingProvider for a specific cloud provider.
|
|
@@ -3529,24 +3721,9 @@ var PricingEngine = class {
|
|
|
3529
3721
|
resourceType,
|
|
3530
3722
|
region
|
|
3531
3723
|
});
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
if (svc === "database" || svc === "rds" || svc === "sql" || svc === "db" || svc === "cloud-sql" || svc === "azure-database") {
|
|
3536
|
-
return p.getDatabasePrice(resourceType, region, attributes.engine);
|
|
3537
|
-
}
|
|
3538
|
-
if (svc === "storage" || svc === "ebs" || svc === "disk" || svc === "gcs" || svc === "managed-disks" || svc === "persistent-disk" || svc === "cloud-storage") {
|
|
3539
|
-
const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
|
|
3540
|
-
return p.getStoragePrice(resourceType, region, sizeGb);
|
|
3541
|
-
}
|
|
3542
|
-
if (svc === "lb" || svc === "load-balancer" || svc === "alb" || svc === "nlb" || svc === "elb" || svc === "cloud-load-balancing") {
|
|
3543
|
-
return p.getLoadBalancerPrice(resourceType, region);
|
|
3544
|
-
}
|
|
3545
|
-
if (svc === "nat" || svc === "nat-gateway" || svc === "cloud-nat") {
|
|
3546
|
-
return p.getNatGatewayPrice(region);
|
|
3547
|
-
}
|
|
3548
|
-
if (svc === "k8s" || svc === "kubernetes" || svc === "eks" || svc === "aks" || svc === "gke") {
|
|
3549
|
-
return p.getKubernetesPrice(region, attributes.mode);
|
|
3724
|
+
const lookup = SERVICE_ALIAS_LOOKUPS.get(svc);
|
|
3725
|
+
if (lookup) {
|
|
3726
|
+
return lookup(p, resourceType, region, attributes);
|
|
3550
3727
|
}
|
|
3551
3728
|
logger.warn("PricingEngine: unrecognised service", { service, provider });
|
|
3552
3729
|
return null;
|
|
@@ -3554,10 +3731,10 @@ var PricingEngine = class {
|
|
|
3554
3731
|
};
|
|
3555
3732
|
|
|
3556
3733
|
// src/tools/analyze-terraform.ts
|
|
3557
|
-
import { z as
|
|
3734
|
+
import { z as z3 } from "zod";
|
|
3558
3735
|
|
|
3559
3736
|
// src/parsers/index.ts
|
|
3560
|
-
import { dirname as dirname2 } from "path";
|
|
3737
|
+
import { dirname as dirname2, isAbsolute, normalize as normalize4, sep as sep2 } from "path";
|
|
3561
3738
|
|
|
3562
3739
|
// src/parsers/hcl-parser.ts
|
|
3563
3740
|
import { parse } from "@cdktf/hcl2json";
|
|
@@ -3595,6 +3772,39 @@ async function parseHclToJson(hclContent, filename = "main.tf") {
|
|
|
3595
3772
|
}
|
|
3596
3773
|
}
|
|
3597
3774
|
|
|
3775
|
+
// src/parsers/extractors/helpers.ts
|
|
3776
|
+
function firstBlock(block, key) {
|
|
3777
|
+
const nested = block[key];
|
|
3778
|
+
if (Array.isArray(nested) && nested.length > 0) {
|
|
3779
|
+
return nested[0];
|
|
3780
|
+
}
|
|
3781
|
+
return {};
|
|
3782
|
+
}
|
|
3783
|
+
function coerceToString(v) {
|
|
3784
|
+
if (typeof v === "string") return v;
|
|
3785
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint" || typeof v === "symbol" || typeof v === "function" || typeof v === "undefined") {
|
|
3786
|
+
return String(v);
|
|
3787
|
+
}
|
|
3788
|
+
return JSON.stringify(v);
|
|
3789
|
+
}
|
|
3790
|
+
function str(v) {
|
|
3791
|
+
return typeof v === "string" && v ? v : void 0;
|
|
3792
|
+
}
|
|
3793
|
+
function num(v) {
|
|
3794
|
+
if (typeof v === "number") return v;
|
|
3795
|
+
if (typeof v === "string") {
|
|
3796
|
+
const n = Number(v);
|
|
3797
|
+
return isNaN(n) ? void 0 : n;
|
|
3798
|
+
}
|
|
3799
|
+
return void 0;
|
|
3800
|
+
}
|
|
3801
|
+
function bool(v) {
|
|
3802
|
+
if (typeof v === "boolean") return v;
|
|
3803
|
+
if (v === "true") return true;
|
|
3804
|
+
if (v === "false") return false;
|
|
3805
|
+
return void 0;
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3598
3808
|
// src/parsers/variable-resolver.ts
|
|
3599
3809
|
function resolveVariables(hclJson, tfvarsContent) {
|
|
3600
3810
|
const defaults = extractVariableDefaults(hclJson);
|
|
@@ -3673,7 +3883,8 @@ function substituteInString(str2, variables) {
|
|
|
3673
3883
|
return str2.replace(VAR_INLINE_RE, (_match, varName) => {
|
|
3674
3884
|
if (varName in variables) {
|
|
3675
3885
|
const v = variables[varName];
|
|
3676
|
-
|
|
3886
|
+
if (v === null || v === void 0) return "";
|
|
3887
|
+
return coerceToString(v);
|
|
3677
3888
|
}
|
|
3678
3889
|
return _match;
|
|
3679
3890
|
});
|
|
@@ -3696,32 +3907,6 @@ function detectProvider(resourceType) {
|
|
|
3696
3907
|
);
|
|
3697
3908
|
}
|
|
3698
3909
|
|
|
3699
|
-
// src/parsers/extractors/helpers.ts
|
|
3700
|
-
function firstBlock(block, key) {
|
|
3701
|
-
const nested = block[key];
|
|
3702
|
-
if (Array.isArray(nested) && nested.length > 0) {
|
|
3703
|
-
return nested[0];
|
|
3704
|
-
}
|
|
3705
|
-
return {};
|
|
3706
|
-
}
|
|
3707
|
-
function str(v) {
|
|
3708
|
-
return typeof v === "string" && v ? v : void 0;
|
|
3709
|
-
}
|
|
3710
|
-
function num(v) {
|
|
3711
|
-
if (typeof v === "number") return v;
|
|
3712
|
-
if (typeof v === "string") {
|
|
3713
|
-
const n = Number(v);
|
|
3714
|
-
return isNaN(n) ? void 0 : n;
|
|
3715
|
-
}
|
|
3716
|
-
return void 0;
|
|
3717
|
-
}
|
|
3718
|
-
function bool(v) {
|
|
3719
|
-
if (typeof v === "boolean") return v;
|
|
3720
|
-
if (v === "true") return true;
|
|
3721
|
-
if (v === "false") return false;
|
|
3722
|
-
return void 0;
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
3910
|
// src/parsers/extractors/aws-extractor.ts
|
|
3726
3911
|
var awsExtractors = {
|
|
3727
3912
|
aws_instance(block) {
|
|
@@ -4307,7 +4492,7 @@ function extractTags(block) {
|
|
|
4307
4492
|
const result = {};
|
|
4308
4493
|
for (const [k, v] of Object.entries(tags)) {
|
|
4309
4494
|
if (typeof v === "string") result[k] = v;
|
|
4310
|
-
else if (v !== null && v !== void 0) result[k] =
|
|
4495
|
+
else if (v !== null && v !== void 0) result[k] = coerceToString(v);
|
|
4311
4496
|
}
|
|
4312
4497
|
return result;
|
|
4313
4498
|
}
|
|
@@ -4447,10 +4632,20 @@ function isWithin(child, parent) {
|
|
|
4447
4632
|
// src/parsers/safe-json.ts
|
|
4448
4633
|
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
4449
4634
|
function safeJsonParse(text) {
|
|
4450
|
-
|
|
4451
|
-
|
|
4635
|
+
let droppedKeys = 0;
|
|
4636
|
+
const parsed = JSON.parse(text, (key, value) => {
|
|
4637
|
+
if (FORBIDDEN_KEYS.has(key)) {
|
|
4638
|
+
droppedKeys++;
|
|
4639
|
+
return void 0;
|
|
4640
|
+
}
|
|
4452
4641
|
return value;
|
|
4453
4642
|
});
|
|
4643
|
+
if (droppedKeys > 0) {
|
|
4644
|
+
logger.warn("safeJsonParse: dropped prototype-pollution keys from payload", {
|
|
4645
|
+
droppedKeys
|
|
4646
|
+
});
|
|
4647
|
+
}
|
|
4648
|
+
return parsed;
|
|
4454
4649
|
}
|
|
4455
4650
|
|
|
4456
4651
|
// src/parsers/module-resolver.ts
|
|
@@ -4461,7 +4656,11 @@ function readTfFiles(dirPath) {
|
|
|
4461
4656
|
let entries;
|
|
4462
4657
|
try {
|
|
4463
4658
|
entries = readdirSync(dirPath);
|
|
4464
|
-
} catch {
|
|
4659
|
+
} catch (err) {
|
|
4660
|
+
logger.warn("Cannot list module directory; skipping", {
|
|
4661
|
+
dirPath: sanitizeForMessage(dirPath),
|
|
4662
|
+
error: sanitizeForMessage(err instanceof Error ? err.message : String(err), 512)
|
|
4663
|
+
});
|
|
4465
4664
|
return [];
|
|
4466
4665
|
}
|
|
4467
4666
|
const files = [];
|
|
@@ -4471,7 +4670,11 @@ function readTfFiles(dirPath) {
|
|
|
4471
4670
|
try {
|
|
4472
4671
|
const content = readFileSync2(fullPath, "utf-8");
|
|
4473
4672
|
files.push({ path: fullPath, content });
|
|
4474
|
-
} catch {
|
|
4673
|
+
} catch (err) {
|
|
4674
|
+
logger.warn("Cannot read module file; skipping", {
|
|
4675
|
+
path: sanitizeForMessage(fullPath),
|
|
4676
|
+
error: sanitizeForMessage(err instanceof Error ? err.message : String(err), 512)
|
|
4677
|
+
});
|
|
4475
4678
|
}
|
|
4476
4679
|
}
|
|
4477
4680
|
return files;
|
|
@@ -4782,7 +4985,7 @@ function extractAttributes(cfnType, props) {
|
|
|
4782
4985
|
return {
|
|
4783
4986
|
instance_type: asString(props.DBInstanceClass),
|
|
4784
4987
|
engine: asString(props.Engine),
|
|
4785
|
-
storage_size_gb:
|
|
4988
|
+
storage_size_gb: num(props.AllocatedStorage),
|
|
4786
4989
|
multi_az: asBoolean(props.MultiAZ)
|
|
4787
4990
|
};
|
|
4788
4991
|
case "AWS::S3::Bucket":
|
|
@@ -4790,8 +4993,8 @@ function extractAttributes(cfnType, props) {
|
|
|
4790
4993
|
case "AWS::EBS::Volume":
|
|
4791
4994
|
return {
|
|
4792
4995
|
storage_type: asString(props.VolumeType),
|
|
4793
|
-
storage_size_gb:
|
|
4794
|
-
iops:
|
|
4996
|
+
storage_size_gb: num(props.Size),
|
|
4997
|
+
iops: num(props.Iops)
|
|
4795
4998
|
};
|
|
4796
4999
|
case "AWS::ElasticLoadBalancingV2::LoadBalancer":
|
|
4797
5000
|
return {
|
|
@@ -4803,16 +5006,16 @@ function extractAttributes(cfnType, props) {
|
|
|
4803
5006
|
return {};
|
|
4804
5007
|
case "AWS::Lambda::Function":
|
|
4805
5008
|
return {
|
|
4806
|
-
memory_size:
|
|
4807
|
-
timeout:
|
|
5009
|
+
memory_size: num(props.MemorySize),
|
|
5010
|
+
timeout: num(props.Timeout)
|
|
4808
5011
|
};
|
|
4809
5012
|
case "AWS::DynamoDB::Table":
|
|
4810
5013
|
return {
|
|
4811
5014
|
billing_mode: asString(props.BillingMode) ?? "PROVISIONED",
|
|
4812
|
-
read_capacity:
|
|
5015
|
+
read_capacity: num(
|
|
4813
5016
|
props.ProvisionedThroughput?.ReadCapacityUnits
|
|
4814
5017
|
),
|
|
4815
|
-
write_capacity:
|
|
5018
|
+
write_capacity: num(
|
|
4816
5019
|
props.ProvisionedThroughput?.WriteCapacityUnits
|
|
4817
5020
|
)
|
|
4818
5021
|
};
|
|
@@ -4827,7 +5030,7 @@ function extractAttributes(cfnType, props) {
|
|
|
4827
5030
|
case "AWS::ElastiCache::CacheCluster":
|
|
4828
5031
|
return {
|
|
4829
5032
|
instance_type: asString(props.CacheNodeType),
|
|
4830
|
-
node_count:
|
|
5033
|
+
node_count: num(props.NumCacheNodes),
|
|
4831
5034
|
engine: asString(props.Engine)
|
|
4832
5035
|
};
|
|
4833
5036
|
case "AWS::CloudFront::Distribution":
|
|
@@ -4842,7 +5045,8 @@ function extractTags2(props) {
|
|
|
4842
5045
|
if (Array.isArray(rawTags)) {
|
|
4843
5046
|
for (const tag of rawTags) {
|
|
4844
5047
|
if (tag && typeof tag === "object" && "Key" in tag && "Value" in tag) {
|
|
4845
|
-
|
|
5048
|
+
const { Key, Value } = tag;
|
|
5049
|
+
tags[coerceToString(Key)] = coerceToString(Value);
|
|
4846
5050
|
}
|
|
4847
5051
|
}
|
|
4848
5052
|
}
|
|
@@ -4851,14 +5055,6 @@ function extractTags2(props) {
|
|
|
4851
5055
|
function asString(value) {
|
|
4852
5056
|
return typeof value === "string" ? value : void 0;
|
|
4853
5057
|
}
|
|
4854
|
-
function asNumber(value) {
|
|
4855
|
-
if (typeof value === "number") return value;
|
|
4856
|
-
if (typeof value === "string") {
|
|
4857
|
-
const n = Number(value);
|
|
4858
|
-
return Number.isNaN(n) ? void 0 : n;
|
|
4859
|
-
}
|
|
4860
|
-
return void 0;
|
|
4861
|
-
}
|
|
4862
5058
|
function asBoolean(value) {
|
|
4863
5059
|
if (typeof value === "boolean") return value;
|
|
4864
5060
|
if (value === "true") return true;
|
|
@@ -4878,11 +5074,11 @@ var CloudFormationParser = class {
|
|
|
4878
5074
|
return false;
|
|
4879
5075
|
});
|
|
4880
5076
|
}
|
|
4881
|
-
|
|
5077
|
+
parse(files, options) {
|
|
4882
5078
|
const warnings = [];
|
|
4883
5079
|
const file = files.find((f) => this.extensions.some((ext) => f.path.endsWith(ext)));
|
|
4884
5080
|
if (!file) {
|
|
4885
|
-
return emptyInventory(warnings);
|
|
5081
|
+
return Promise.resolve(emptyInventory(warnings));
|
|
4886
5082
|
}
|
|
4887
5083
|
let template;
|
|
4888
5084
|
try {
|
|
@@ -4891,11 +5087,11 @@ var CloudFormationParser = class {
|
|
|
4891
5087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4892
5088
|
warnings.push(`Failed to parse CloudFormation template ${file.path}: ${msg}`);
|
|
4893
5089
|
logger.warn("CloudFormation parse error", { path: file.path, error: msg });
|
|
4894
|
-
return emptyInventory(warnings);
|
|
5090
|
+
return Promise.resolve(emptyInventory(warnings));
|
|
4895
5091
|
}
|
|
4896
5092
|
if (!template || typeof template !== "object" || !template.Resources) {
|
|
4897
5093
|
warnings.push(`Invalid CloudFormation template: missing Resources section in ${file.path}`);
|
|
4898
|
-
return emptyInventory(warnings);
|
|
5094
|
+
return Promise.resolve(emptyInventory(warnings));
|
|
4899
5095
|
}
|
|
4900
5096
|
const resolvedParams = resolveParameters(
|
|
4901
5097
|
template.Parameters ?? {},
|
|
@@ -4928,7 +5124,7 @@ var CloudFormationParser = class {
|
|
|
4928
5124
|
region,
|
|
4929
5125
|
warningCount: warnings.length
|
|
4930
5126
|
});
|
|
4931
|
-
return inventory;
|
|
5127
|
+
return Promise.resolve(inventory);
|
|
4932
5128
|
}
|
|
4933
5129
|
};
|
|
4934
5130
|
function emptyInventory(warnings) {
|
|
@@ -5034,14 +5230,14 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
|
|
|
5034
5230
|
const diskProps = props;
|
|
5035
5231
|
return {
|
|
5036
5232
|
sku: resource.sku?.name,
|
|
5037
|
-
storage_size_gb:
|
|
5233
|
+
storage_size_gb: num(diskProps.diskSizeGB)
|
|
5038
5234
|
};
|
|
5039
5235
|
}
|
|
5040
5236
|
case "microsoft.dbforpostgresql/flexibleservers": {
|
|
5041
5237
|
const storage = props.storage;
|
|
5042
5238
|
return {
|
|
5043
5239
|
sku: resource.sku?.name,
|
|
5044
|
-
storage_size_gb:
|
|
5240
|
+
storage_size_gb: num(storage?.storageSizeGB)
|
|
5045
5241
|
};
|
|
5046
5242
|
}
|
|
5047
5243
|
case "microsoft.dbformysql/flexibleservers":
|
|
@@ -5065,7 +5261,7 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
|
|
|
5065
5261
|
const pools = props.agentPoolProfiles;
|
|
5066
5262
|
const firstPool = pools?.[0];
|
|
5067
5263
|
return {
|
|
5068
|
-
node_count:
|
|
5264
|
+
node_count: num(firstPool?.count),
|
|
5069
5265
|
vm_size: typeof firstPool?.vmSize === "string" ? firstPool.vmSize : void 0
|
|
5070
5266
|
};
|
|
5071
5267
|
}
|
|
@@ -5099,14 +5295,6 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
|
|
|
5099
5295
|
return {};
|
|
5100
5296
|
}
|
|
5101
5297
|
}
|
|
5102
|
-
function asNumber2(value) {
|
|
5103
|
-
if (typeof value === "number") return value;
|
|
5104
|
-
if (typeof value === "string") {
|
|
5105
|
-
const n = Number(value);
|
|
5106
|
-
return Number.isNaN(n) ? void 0 : n;
|
|
5107
|
-
}
|
|
5108
|
-
return void 0;
|
|
5109
|
-
}
|
|
5110
5298
|
|
|
5111
5299
|
// src/parsers/bicep/arm-parser.ts
|
|
5112
5300
|
var ArmParser = class {
|
|
@@ -5118,11 +5306,11 @@ var ArmParser = class {
|
|
|
5118
5306
|
return f.content.includes("schema.management.azure.com") || f.content.includes("Microsoft.") && f.content.includes("apiVersion");
|
|
5119
5307
|
});
|
|
5120
5308
|
}
|
|
5121
|
-
|
|
5309
|
+
parse(files, options) {
|
|
5122
5310
|
const warnings = [];
|
|
5123
5311
|
const file = files.find((f) => f.path.endsWith(".json"));
|
|
5124
5312
|
if (!file) {
|
|
5125
|
-
return emptyInventory2(warnings);
|
|
5313
|
+
return Promise.resolve(emptyInventory2(warnings));
|
|
5126
5314
|
}
|
|
5127
5315
|
let template;
|
|
5128
5316
|
try {
|
|
@@ -5131,11 +5319,11 @@ var ArmParser = class {
|
|
|
5131
5319
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5132
5320
|
warnings.push(`Failed to parse ARM template ${file.path}: ${msg}`);
|
|
5133
5321
|
logger.warn("ARM template parse error", { path: file.path, error: msg });
|
|
5134
|
-
return emptyInventory2(warnings);
|
|
5322
|
+
return Promise.resolve(emptyInventory2(warnings));
|
|
5135
5323
|
}
|
|
5136
5324
|
if (!template || typeof template !== "object" || !Array.isArray(template.resources)) {
|
|
5137
5325
|
warnings.push(`Invalid ARM template: missing resources array in ${file.path}`);
|
|
5138
|
-
return emptyInventory2(warnings);
|
|
5326
|
+
return Promise.resolve(emptyInventory2(warnings));
|
|
5139
5327
|
}
|
|
5140
5328
|
const resolvedParams = resolveParameters2(
|
|
5141
5329
|
template.parameters ?? {},
|
|
@@ -5168,7 +5356,7 @@ var ArmParser = class {
|
|
|
5168
5356
|
region,
|
|
5169
5357
|
warningCount: warnings.length
|
|
5170
5358
|
});
|
|
5171
|
-
return inventory;
|
|
5359
|
+
return Promise.resolve(inventory);
|
|
5172
5360
|
}
|
|
5173
5361
|
};
|
|
5174
5362
|
function emptyInventory2(warnings) {
|
|
@@ -5318,7 +5506,7 @@ function extractAttributes3(pulumiType, props) {
|
|
|
5318
5506
|
return {
|
|
5319
5507
|
instance_type: asString2(props.instanceClass),
|
|
5320
5508
|
engine: asString2(props.engine),
|
|
5321
|
-
storage_size_gb:
|
|
5509
|
+
storage_size_gb: num(props.allocatedStorage),
|
|
5322
5510
|
multi_az: asBoolean2(props.multiAz)
|
|
5323
5511
|
};
|
|
5324
5512
|
case "aws:s3/bucket:Bucket":
|
|
@@ -5326,8 +5514,8 @@ function extractAttributes3(pulumiType, props) {
|
|
|
5326
5514
|
case "aws:ebs/volume:Volume":
|
|
5327
5515
|
return {
|
|
5328
5516
|
storage_type: asString2(props.type),
|
|
5329
|
-
storage_size_gb:
|
|
5330
|
-
iops:
|
|
5517
|
+
storage_size_gb: num(props.size),
|
|
5518
|
+
iops: num(props.iops)
|
|
5331
5519
|
};
|
|
5332
5520
|
case "aws:lb/loadBalancer:LoadBalancer":
|
|
5333
5521
|
return {
|
|
@@ -5339,14 +5527,14 @@ function extractAttributes3(pulumiType, props) {
|
|
|
5339
5527
|
return {};
|
|
5340
5528
|
case "aws:lambda/function:Function":
|
|
5341
5529
|
return {
|
|
5342
|
-
memory_size:
|
|
5343
|
-
timeout:
|
|
5530
|
+
memory_size: num(props.memorySize),
|
|
5531
|
+
timeout: num(props.timeout)
|
|
5344
5532
|
};
|
|
5345
5533
|
case "aws:dynamodb/table:Table":
|
|
5346
5534
|
return {
|
|
5347
5535
|
billing_mode: asString2(props.billingMode) ?? "PROVISIONED",
|
|
5348
|
-
read_capacity:
|
|
5349
|
-
write_capacity:
|
|
5536
|
+
read_capacity: num(props.readCapacity),
|
|
5537
|
+
write_capacity: num(props.writeCapacity)
|
|
5350
5538
|
};
|
|
5351
5539
|
case "aws:route53/zone:Zone":
|
|
5352
5540
|
return {};
|
|
@@ -5392,12 +5580,12 @@ function extractAttributes3(pulumiType, props) {
|
|
|
5392
5580
|
};
|
|
5393
5581
|
case "gcp:container:Cluster":
|
|
5394
5582
|
return {
|
|
5395
|
-
node_count:
|
|
5583
|
+
node_count: num(props.initialNodeCount)
|
|
5396
5584
|
};
|
|
5397
5585
|
case "gcp:cloudfunctions:Function":
|
|
5398
5586
|
return {
|
|
5399
|
-
memory_size:
|
|
5400
|
-
timeout:
|
|
5587
|
+
memory_size: num(props.availableMemoryMb),
|
|
5588
|
+
timeout: num(props.timeout)
|
|
5401
5589
|
};
|
|
5402
5590
|
case "gcp:dns:ManagedZone":
|
|
5403
5591
|
return {};
|
|
@@ -5420,14 +5608,6 @@ function extractTags3(props) {
|
|
|
5420
5608
|
function asString2(value) {
|
|
5421
5609
|
return typeof value === "string" ? value : void 0;
|
|
5422
5610
|
}
|
|
5423
|
-
function asNumber3(value) {
|
|
5424
|
-
if (typeof value === "number") return value;
|
|
5425
|
-
if (typeof value === "string") {
|
|
5426
|
-
const n = Number(value);
|
|
5427
|
-
return Number.isNaN(n) ? void 0 : n;
|
|
5428
|
-
}
|
|
5429
|
-
return void 0;
|
|
5430
|
-
}
|
|
5431
5611
|
function asBoolean2(value) {
|
|
5432
5612
|
if (typeof value === "boolean") return value;
|
|
5433
5613
|
if (value === "true") return true;
|
|
@@ -5450,7 +5630,7 @@ var PulumiParser = class {
|
|
|
5450
5630
|
}
|
|
5451
5631
|
});
|
|
5452
5632
|
}
|
|
5453
|
-
|
|
5633
|
+
parse(files, _options) {
|
|
5454
5634
|
const warnings = [];
|
|
5455
5635
|
const file = files.find((f) => {
|
|
5456
5636
|
try {
|
|
@@ -5462,7 +5642,7 @@ var PulumiParser = class {
|
|
|
5462
5642
|
}
|
|
5463
5643
|
});
|
|
5464
5644
|
if (!file) {
|
|
5465
|
-
return emptyInventory3(warnings);
|
|
5645
|
+
return Promise.resolve(emptyInventory3(warnings));
|
|
5466
5646
|
}
|
|
5467
5647
|
let stackExport;
|
|
5468
5648
|
try {
|
|
@@ -5471,11 +5651,11 @@ var PulumiParser = class {
|
|
|
5471
5651
|
const msg = err instanceof Error ? err.message : String(err);
|
|
5472
5652
|
warnings.push(`Failed to parse Pulumi stack export ${file.path}: ${msg}`);
|
|
5473
5653
|
logger.warn("Pulumi parse error", { path: file.path, error: msg });
|
|
5474
|
-
return emptyInventory3(warnings);
|
|
5654
|
+
return Promise.resolve(emptyInventory3(warnings));
|
|
5475
5655
|
}
|
|
5476
5656
|
if (!stackExport?.deployment?.resources || !Array.isArray(stackExport.deployment.resources)) {
|
|
5477
5657
|
warnings.push(`Invalid Pulumi stack export: missing deployment.resources in ${file.path}`);
|
|
5478
|
-
return emptyInventory3(warnings);
|
|
5658
|
+
return Promise.resolve(emptyInventory3(warnings));
|
|
5479
5659
|
}
|
|
5480
5660
|
const resources = [];
|
|
5481
5661
|
for (const pulumiRes of stackExport.deployment.resources) {
|
|
@@ -5509,7 +5689,7 @@ var PulumiParser = class {
|
|
|
5509
5689
|
region: dominantRegion,
|
|
5510
5690
|
warningCount: warnings.length
|
|
5511
5691
|
});
|
|
5512
|
-
return inventory;
|
|
5692
|
+
return Promise.resolve(inventory);
|
|
5513
5693
|
}
|
|
5514
5694
|
};
|
|
5515
5695
|
function emptyInventory3(warnings) {
|
|
@@ -5606,7 +5786,7 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
|
|
|
5606
5786
|
parse_warnings: warnings
|
|
5607
5787
|
};
|
|
5608
5788
|
}
|
|
5609
|
-
const combined =
|
|
5789
|
+
const combined = mergeHclJsons(parsedJsons.map((p) => p.json));
|
|
5610
5790
|
const variables = resolveVariables(combined, tfvarsContent);
|
|
5611
5791
|
const providers = ["aws", "azure", "gcp"];
|
|
5612
5792
|
const defaultRegions = {};
|
|
@@ -5625,7 +5805,7 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
|
|
|
5625
5805
|
}
|
|
5626
5806
|
}
|
|
5627
5807
|
if (resolveModulesEnabled) {
|
|
5628
|
-
const moduleBasePath = basePath ?? (parsedJsons.length > 0 ?
|
|
5808
|
+
const moduleBasePath = basePath ?? (parsedJsons.length > 0 ? deriveModuleBasePath(parsedJsons[0].path) : process.cwd());
|
|
5629
5809
|
try {
|
|
5630
5810
|
const moduleResources = await resolveModules(combined, moduleBasePath, variables, warnings);
|
|
5631
5811
|
allResources.push(...moduleResources);
|
|
@@ -5658,46 +5838,19 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
|
|
|
5658
5838
|
});
|
|
5659
5839
|
return inventory;
|
|
5660
5840
|
}
|
|
5661
|
-
function
|
|
5662
|
-
const
|
|
5663
|
-
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
continue;
|
|
5668
|
-
}
|
|
5669
|
-
const existing = merged[key];
|
|
5670
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
5671
|
-
merged[key] = mergeObjects2(
|
|
5672
|
-
existing,
|
|
5673
|
-
value
|
|
5674
|
-
);
|
|
5675
|
-
} else if (Array.isArray(existing) && Array.isArray(value)) {
|
|
5676
|
-
merged[key] = [...existing, ...value];
|
|
5677
|
-
} else {
|
|
5678
|
-
merged[key] = deepClone(value);
|
|
5679
|
-
}
|
|
5680
|
-
}
|
|
5841
|
+
function deriveModuleBasePath(filePath) {
|
|
5842
|
+
const normalized = normalize4(filePath);
|
|
5843
|
+
const isWindowsDrive = /^[A-Za-z]:/.test(normalized);
|
|
5844
|
+
const isWindowsUnc = normalized.startsWith("\\\\") || normalized.startsWith("//");
|
|
5845
|
+
if (isAbsolute(normalized) || isWindowsDrive || isWindowsUnc) {
|
|
5846
|
+
return process.cwd();
|
|
5681
5847
|
}
|
|
5682
|
-
|
|
5683
|
-
}
|
|
5684
|
-
|
|
5685
|
-
|
|
5686
|
-
for (const [key, bVal] of Object.entries(b)) {
|
|
5687
|
-
const aVal = result[key];
|
|
5688
|
-
if (aVal !== null && typeof aVal === "object" && !Array.isArray(aVal) && bVal !== null && typeof bVal === "object" && !Array.isArray(bVal)) {
|
|
5689
|
-
result[key] = mergeObjects2(aVal, bVal);
|
|
5690
|
-
} else if (Array.isArray(aVal) && Array.isArray(bVal)) {
|
|
5691
|
-
result[key] = [...aVal, ...bVal];
|
|
5692
|
-
} else {
|
|
5693
|
-
result[key] = deepClone(bVal);
|
|
5694
|
-
}
|
|
5848
|
+
const dir = dirname2(normalized);
|
|
5849
|
+
const escapesUpward = dir === ".." || dir.startsWith(`..${sep2}`) || dir.startsWith("../");
|
|
5850
|
+
if (escapesUpward) {
|
|
5851
|
+
return process.cwd();
|
|
5695
5852
|
}
|
|
5696
|
-
return
|
|
5697
|
-
}
|
|
5698
|
-
function deepClone(value) {
|
|
5699
|
-
if (value === null || typeof value !== "object") return value;
|
|
5700
|
-
return structuredClone(value);
|
|
5853
|
+
return dir;
|
|
5701
5854
|
}
|
|
5702
5855
|
|
|
5703
5856
|
// src/parsers/dependency-graph.ts
|
|
@@ -5828,6 +5981,9 @@ function generateMermaidDiagram(graph) {
|
|
|
5828
5981
|
return lines.join("\n");
|
|
5829
5982
|
}
|
|
5830
5983
|
|
|
5984
|
+
// src/schemas/fragments.ts
|
|
5985
|
+
import { z as z2 } from "zod";
|
|
5986
|
+
|
|
5831
5987
|
// src/schemas/bounded.ts
|
|
5832
5988
|
import { z } from "zod";
|
|
5833
5989
|
var MAX_FILE_PATH_LEN = 1024;
|
|
@@ -5836,23 +5992,43 @@ var MAX_TFVARS_BYTES = 1 * 1024 * 1024;
|
|
|
5836
5992
|
var MAX_PLAN_JSON_BYTES = 20 * 1024 * 1024;
|
|
5837
5993
|
var MAX_STATE_JSON_BYTES = 20 * 1024 * 1024;
|
|
5838
5994
|
var MAX_SHORT_STRING_LEN = 256;
|
|
5995
|
+
var MAX_TOTAL_FILE_BYTES = 50 * 1024 * 1024;
|
|
5839
5996
|
var filePathSchema = z.string().max(MAX_FILE_PATH_LEN, `File path exceeds ${MAX_FILE_PATH_LEN} characters`);
|
|
5840
5997
|
var fileContentSchema = z.string().max(MAX_FILE_CONTENT_BYTES, `File content exceeds ${MAX_FILE_CONTENT_BYTES} bytes`);
|
|
5841
5998
|
var tfvarsSchema = z.string().max(MAX_TFVARS_BYTES, `tfvars content exceeds ${MAX_TFVARS_BYTES} bytes`);
|
|
5842
5999
|
var planJsonSchema = z.string().max(MAX_PLAN_JSON_BYTES, `plan JSON exceeds ${MAX_PLAN_JSON_BYTES} bytes`);
|
|
5843
6000
|
var stateJsonSchema = z.string().max(MAX_STATE_JSON_BYTES, `state JSON exceeds ${MAX_STATE_JSON_BYTES} bytes`);
|
|
5844
6001
|
var shortStringSchema = z.string().max(MAX_SHORT_STRING_LEN, `value exceeds ${MAX_SHORT_STRING_LEN} characters`);
|
|
6002
|
+
function assertTotalFileBytesWithin(files, ctx) {
|
|
6003
|
+
let total = 0;
|
|
6004
|
+
for (const f of files) {
|
|
6005
|
+
total += f.content.length;
|
|
6006
|
+
if (total > MAX_TOTAL_FILE_BYTES) {
|
|
6007
|
+
ctx.addIssue({
|
|
6008
|
+
code: z.ZodIssueCode.custom,
|
|
6009
|
+
message: `aggregate file content exceeds ${MAX_TOTAL_FILE_BYTES} bytes`,
|
|
6010
|
+
path: ["files"]
|
|
6011
|
+
});
|
|
6012
|
+
return;
|
|
6013
|
+
}
|
|
6014
|
+
}
|
|
6015
|
+
}
|
|
6016
|
+
|
|
6017
|
+
// src/schemas/fragments.ts
|
|
6018
|
+
var providerEnum = z2.enum(["aws", "azure", "gcp"]);
|
|
6019
|
+
var iacFileEntrySchema = z2.object({
|
|
6020
|
+
path: filePathSchema.describe("File path"),
|
|
6021
|
+
content: fileContentSchema.describe("File content")
|
|
6022
|
+
});
|
|
6023
|
+
var iacFilesSchema = z2.array(iacFileEntrySchema).max(2e3, "files array exceeds 2000 entries").superRefine(assertTotalFileBytesWithin);
|
|
6024
|
+
var tfvarsField = tfvarsSchema.optional().describe("Contents of terraform.tfvars file");
|
|
6025
|
+
var currencyField = z2.enum(SUPPORTED_CURRENCIES).optional().default("USD").describe("Output currency for cost estimates. Defaults to USD.");
|
|
5845
6026
|
|
|
5846
6027
|
// src/tools/analyze-terraform.ts
|
|
5847
|
-
var analyzeTerraformSchema =
|
|
5848
|
-
files:
|
|
5849
|
-
|
|
5850
|
-
|
|
5851
|
-
content: fileContentSchema.describe("File content (HCL)")
|
|
5852
|
-
})
|
|
5853
|
-
).max(2e3, "files array exceeds 2000 entries").describe("Terraform files to analyze"),
|
|
5854
|
-
tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
|
|
5855
|
-
include_dependencies: z2.boolean().optional().default(false).describe(
|
|
6028
|
+
var analyzeTerraformSchema = z3.object({
|
|
6029
|
+
files: iacFilesSchema.describe("Terraform files to analyze"),
|
|
6030
|
+
tfvars: tfvarsField,
|
|
6031
|
+
include_dependencies: z3.boolean().optional().default(false).describe(
|
|
5856
6032
|
"When true, parse resource references and depends_on blocks to build a dependency graph and Mermaid diagram"
|
|
5857
6033
|
)
|
|
5858
6034
|
});
|
|
@@ -5904,7 +6080,7 @@ async function analyzeTerraform(params) {
|
|
|
5904
6080
|
}
|
|
5905
6081
|
|
|
5906
6082
|
// src/tools/estimate-cost.ts
|
|
5907
|
-
import { z as
|
|
6083
|
+
import { z as z4 } from "zod";
|
|
5908
6084
|
|
|
5909
6085
|
// src/mapping/region-mapper.ts
|
|
5910
6086
|
var DEFAULT_REGIONS = {
|
|
@@ -5933,6 +6109,27 @@ function mapRegion(region, sourceProvider, targetProvider) {
|
|
|
5933
6109
|
return mapped;
|
|
5934
6110
|
}
|
|
5935
6111
|
|
|
6112
|
+
// src/calculator/estimate-factory.ts
|
|
6113
|
+
function round2(n) {
|
|
6114
|
+
return Math.round(n * 100) / 100;
|
|
6115
|
+
}
|
|
6116
|
+
function makeCostEstimate(input) {
|
|
6117
|
+
return {
|
|
6118
|
+
resource_id: input.resource.id,
|
|
6119
|
+
resource_type: input.resource.type,
|
|
6120
|
+
resource_name: input.resource.name,
|
|
6121
|
+
provider: input.provider,
|
|
6122
|
+
region: input.region,
|
|
6123
|
+
monthly_cost: round2(input.monthlyCost),
|
|
6124
|
+
yearly_cost: round2(input.monthlyCost * 12),
|
|
6125
|
+
currency: input.currency ?? "USD",
|
|
6126
|
+
breakdown: input.breakdown,
|
|
6127
|
+
confidence: input.confidence,
|
|
6128
|
+
notes: input.notes,
|
|
6129
|
+
pricing_source: input.pricingSource
|
|
6130
|
+
};
|
|
6131
|
+
}
|
|
6132
|
+
|
|
5936
6133
|
// src/mapping/instance-mapper.ts
|
|
5937
6134
|
function directionKey(source, target) {
|
|
5938
6135
|
if (source === target) return null;
|
|
@@ -6114,7 +6311,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
6114
6311
|
`No instance mapping found for ${sourceInstanceType} (${resource.provider} -> ${targetProvider})`
|
|
6115
6312
|
);
|
|
6116
6313
|
} else {
|
|
6117
|
-
const { getInstanceMap: getInstanceMap2 } = await import("./loader-
|
|
6314
|
+
const { getInstanceMap: getInstanceMap2 } = await import("./loader-CMBV7346.js");
|
|
6118
6315
|
const im = getInstanceMap2();
|
|
6119
6316
|
const dirKey = `${resource.provider}_to_${targetProvider}`;
|
|
6120
6317
|
const directMap = im[dirKey];
|
|
@@ -6128,11 +6325,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
6128
6325
|
}
|
|
6129
6326
|
}
|
|
6130
6327
|
const effectiveInstance = mappedInstance ?? sourceInstanceType;
|
|
6131
|
-
const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(
|
|
6132
|
-
effectiveInstance,
|
|
6133
|
-
targetRegion,
|
|
6134
|
-
resource.attributes.os
|
|
6135
|
-
) : null;
|
|
6328
|
+
const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(effectiveInstance, targetRegion, resource.attributes.os) : null;
|
|
6136
6329
|
let computeMonthlyCost = 0;
|
|
6137
6330
|
let pricingSource = "fallback";
|
|
6138
6331
|
if (computePrice) {
|
|
@@ -6253,20 +6446,16 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
6253
6446
|
}
|
|
6254
6447
|
}
|
|
6255
6448
|
const totalMonthly = computeMonthlyCost + storageMonthlyCost;
|
|
6256
|
-
return {
|
|
6257
|
-
|
|
6258
|
-
resource_type: resource.type,
|
|
6259
|
-
resource_name: resource.name,
|
|
6449
|
+
return makeCostEstimate({
|
|
6450
|
+
resource,
|
|
6260
6451
|
provider: targetProvider,
|
|
6261
6452
|
region: targetRegion,
|
|
6262
|
-
|
|
6263
|
-
yearly_cost: totalMonthly * 12,
|
|
6264
|
-
currency: "USD",
|
|
6453
|
+
monthlyCost: totalMonthly,
|
|
6265
6454
|
breakdown,
|
|
6266
6455
|
confidence,
|
|
6267
6456
|
notes,
|
|
6268
|
-
|
|
6269
|
-
};
|
|
6457
|
+
pricingSource
|
|
6458
|
+
});
|
|
6270
6459
|
}
|
|
6271
6460
|
|
|
6272
6461
|
// src/calculator/database.ts
|
|
@@ -6369,20 +6558,16 @@ async function calculateDatabaseCost(resource, targetProvider, targetRegion, pri
|
|
|
6369
6558
|
}
|
|
6370
6559
|
}
|
|
6371
6560
|
const totalMonthly = instanceMonthlyCost + storageMonthlyCost;
|
|
6372
|
-
return {
|
|
6373
|
-
|
|
6374
|
-
resource_type: resource.type,
|
|
6375
|
-
resource_name: resource.name,
|
|
6561
|
+
return makeCostEstimate({
|
|
6562
|
+
resource,
|
|
6376
6563
|
provider: targetProvider,
|
|
6377
6564
|
region: targetRegion,
|
|
6378
|
-
|
|
6379
|
-
yearly_cost: totalMonthly * 12,
|
|
6380
|
-
currency: "USD",
|
|
6565
|
+
monthlyCost: totalMonthly,
|
|
6381
6566
|
breakdown,
|
|
6382
6567
|
confidence: mappedInstance ? "high" : "low",
|
|
6383
6568
|
notes,
|
|
6384
|
-
|
|
6385
|
-
};
|
|
6569
|
+
pricingSource
|
|
6570
|
+
});
|
|
6386
6571
|
}
|
|
6387
6572
|
|
|
6388
6573
|
// src/calculator/storage.ts
|
|
@@ -6461,20 +6646,16 @@ async function calculateStorageCost(resource, targetProvider, targetRegion, pric
|
|
|
6461
6646
|
targetRegion
|
|
6462
6647
|
});
|
|
6463
6648
|
}
|
|
6464
|
-
return {
|
|
6465
|
-
|
|
6466
|
-
resource_type: resource.type,
|
|
6467
|
-
resource_name: resource.name,
|
|
6649
|
+
return makeCostEstimate({
|
|
6650
|
+
resource,
|
|
6468
6651
|
provider: targetProvider,
|
|
6469
6652
|
region: targetRegion,
|
|
6470
|
-
|
|
6471
|
-
yearly_cost: totalMonthly * 12,
|
|
6472
|
-
currency: "USD",
|
|
6653
|
+
monthlyCost: totalMonthly,
|
|
6473
6654
|
breakdown,
|
|
6474
6655
|
confidence: storagePrice ? "high" : "low",
|
|
6475
6656
|
notes,
|
|
6476
|
-
|
|
6477
|
-
};
|
|
6657
|
+
pricingSource
|
|
6658
|
+
});
|
|
6478
6659
|
}
|
|
6479
6660
|
|
|
6480
6661
|
// src/calculator/network.ts
|
|
@@ -6524,20 +6705,16 @@ async function calculateNatGatewayCost(resource, targetProvider, targetRegion, p
|
|
|
6524
6705
|
} else {
|
|
6525
6706
|
notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
|
|
6526
6707
|
}
|
|
6527
|
-
return {
|
|
6528
|
-
|
|
6529
|
-
resource_type: resource.type,
|
|
6530
|
-
resource_name: resource.name,
|
|
6708
|
+
return makeCostEstimate({
|
|
6709
|
+
resource,
|
|
6531
6710
|
provider: targetProvider,
|
|
6532
6711
|
region: targetRegion,
|
|
6533
|
-
|
|
6534
|
-
yearly_cost: totalMonthly * 12,
|
|
6535
|
-
currency: "USD",
|
|
6712
|
+
monthlyCost: totalMonthly,
|
|
6536
6713
|
breakdown,
|
|
6537
6714
|
confidence: natPrice ? "medium" : "low",
|
|
6538
6715
|
notes,
|
|
6539
|
-
|
|
6540
|
-
};
|
|
6716
|
+
pricingSource: natPricingSource
|
|
6717
|
+
});
|
|
6541
6718
|
}
|
|
6542
6719
|
async function calculateLoadBalancerCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
|
|
6543
6720
|
const notes = [];
|
|
@@ -6569,20 +6746,16 @@ async function calculateLoadBalancerCost(resource, targetProvider, targetRegion,
|
|
|
6569
6746
|
} else {
|
|
6570
6747
|
notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
|
|
6571
6748
|
}
|
|
6572
|
-
return {
|
|
6573
|
-
|
|
6574
|
-
resource_type: resource.type,
|
|
6575
|
-
resource_name: resource.name,
|
|
6749
|
+
return makeCostEstimate({
|
|
6750
|
+
resource,
|
|
6576
6751
|
provider: targetProvider,
|
|
6577
6752
|
region: targetRegion,
|
|
6578
|
-
|
|
6579
|
-
yearly_cost: totalMonthly * 12,
|
|
6580
|
-
currency: "USD",
|
|
6753
|
+
monthlyCost: totalMonthly,
|
|
6581
6754
|
breakdown,
|
|
6582
6755
|
confidence: lbPrice ? "medium" : "low",
|
|
6583
6756
|
notes,
|
|
6584
|
-
|
|
6585
|
-
};
|
|
6757
|
+
pricingSource: lbPricingSource
|
|
6758
|
+
});
|
|
6586
6759
|
}
|
|
6587
6760
|
|
|
6588
6761
|
// src/calculator/kubernetes.ts
|
|
@@ -6653,20 +6826,16 @@ async function calculateKubernetesCost(resource, targetProvider, targetRegion, p
|
|
|
6653
6826
|
);
|
|
6654
6827
|
}
|
|
6655
6828
|
}
|
|
6656
|
-
return {
|
|
6657
|
-
|
|
6658
|
-
resource_type: resource.type,
|
|
6659
|
-
resource_name: resource.name,
|
|
6829
|
+
return makeCostEstimate({
|
|
6830
|
+
resource,
|
|
6660
6831
|
provider: targetProvider,
|
|
6661
6832
|
region: targetRegion,
|
|
6662
|
-
|
|
6663
|
-
yearly_cost: totalMonthly * 12,
|
|
6664
|
-
currency: "USD",
|
|
6833
|
+
monthlyCost: totalMonthly,
|
|
6665
6834
|
breakdown,
|
|
6666
6835
|
confidence: k8sPrice ? "medium" : "low",
|
|
6667
6836
|
notes,
|
|
6668
|
-
|
|
6669
|
-
};
|
|
6837
|
+
pricingSource
|
|
6838
|
+
});
|
|
6670
6839
|
}
|
|
6671
6840
|
|
|
6672
6841
|
// src/calculator/serverless.ts
|
|
@@ -6721,20 +6890,16 @@ function calculateLambdaCost(resource, targetProvider, targetRegion) {
|
|
|
6721
6890
|
}
|
|
6722
6891
|
const totalMonthly = requestCost + computeCost;
|
|
6723
6892
|
void timeoutSec;
|
|
6724
|
-
return {
|
|
6725
|
-
|
|
6726
|
-
resource_type: resource.type,
|
|
6727
|
-
resource_name: resource.name,
|
|
6893
|
+
return makeCostEstimate({
|
|
6894
|
+
resource,
|
|
6728
6895
|
provider: targetProvider,
|
|
6729
6896
|
region: targetRegion,
|
|
6730
|
-
|
|
6731
|
-
yearly_cost: totalMonthly * 12,
|
|
6732
|
-
currency: "USD",
|
|
6897
|
+
monthlyCost: totalMonthly,
|
|
6733
6898
|
breakdown,
|
|
6734
6899
|
confidence: "medium",
|
|
6735
6900
|
notes,
|
|
6736
|
-
|
|
6737
|
-
};
|
|
6901
|
+
pricingSource: "fallback"
|
|
6902
|
+
});
|
|
6738
6903
|
}
|
|
6739
6904
|
var DYNAMO_ON_DEMAND_WRITE_PER_MILLION = 1.25;
|
|
6740
6905
|
var DYNAMO_ON_DEMAND_READ_PER_MILLION = 0.25;
|
|
@@ -6804,20 +6969,16 @@ function calculateDynamoDbCost(resource, targetProvider, targetRegion) {
|
|
|
6804
6969
|
monthly_cost: storageCost
|
|
6805
6970
|
});
|
|
6806
6971
|
totalMonthly += storageCost;
|
|
6807
|
-
return {
|
|
6808
|
-
|
|
6809
|
-
resource_type: resource.type,
|
|
6810
|
-
resource_name: resource.name,
|
|
6972
|
+
return makeCostEstimate({
|
|
6973
|
+
resource,
|
|
6811
6974
|
provider: targetProvider,
|
|
6812
6975
|
region: targetRegion,
|
|
6813
|
-
|
|
6814
|
-
yearly_cost: totalMonthly * 12,
|
|
6815
|
-
currency: "USD",
|
|
6976
|
+
monthlyCost: totalMonthly,
|
|
6816
6977
|
breakdown,
|
|
6817
6978
|
confidence: "medium",
|
|
6818
6979
|
notes,
|
|
6819
|
-
|
|
6820
|
-
};
|
|
6980
|
+
pricingSource: "fallback"
|
|
6981
|
+
});
|
|
6821
6982
|
}
|
|
6822
6983
|
var SQS_STANDARD_PER_MILLION = 0.4;
|
|
6823
6984
|
var SQS_FIFO_PER_MILLION = 0.5;
|
|
@@ -6842,20 +7003,16 @@ function calculateSqsCost(resource, targetProvider, targetRegion) {
|
|
|
6842
7003
|
);
|
|
6843
7004
|
}
|
|
6844
7005
|
notes.push(`SQS ${queueType} queue pricing at $${pricePerMillion}/million requests`);
|
|
6845
|
-
return {
|
|
6846
|
-
|
|
6847
|
-
resource_type: resource.type,
|
|
6848
|
-
resource_name: resource.name,
|
|
7006
|
+
return makeCostEstimate({
|
|
7007
|
+
resource,
|
|
6849
7008
|
provider: targetProvider,
|
|
6850
7009
|
region: targetRegion,
|
|
6851
|
-
|
|
6852
|
-
yearly_cost: totalMonthly * 12,
|
|
6853
|
-
currency: "USD",
|
|
7010
|
+
monthlyCost: totalMonthly,
|
|
6854
7011
|
breakdown,
|
|
6855
7012
|
confidence: "medium",
|
|
6856
7013
|
notes,
|
|
6857
|
-
|
|
6858
|
-
};
|
|
7014
|
+
pricingSource: "fallback"
|
|
7015
|
+
});
|
|
6859
7016
|
}
|
|
6860
7017
|
|
|
6861
7018
|
// src/calculator/cache.ts
|
|
@@ -6912,20 +7069,16 @@ function calculateElastiCacheCost(resource, targetProvider, targetRegion) {
|
|
|
6912
7069
|
} else {
|
|
6913
7070
|
notes.push(`No pricing data found for ElastiCache node type ${nodeType}; cost reported as $0`);
|
|
6914
7071
|
}
|
|
6915
|
-
return {
|
|
6916
|
-
|
|
6917
|
-
resource_type: resource.type,
|
|
6918
|
-
resource_name: resource.name,
|
|
7072
|
+
return makeCostEstimate({
|
|
7073
|
+
resource,
|
|
6919
7074
|
provider: targetProvider,
|
|
6920
7075
|
region: targetRegion,
|
|
6921
|
-
|
|
6922
|
-
yearly_cost: totalMonthly * 12,
|
|
6923
|
-
currency: "USD",
|
|
7076
|
+
monthlyCost: totalMonthly,
|
|
6924
7077
|
breakdown,
|
|
6925
7078
|
confidence: basePrice !== void 0 ? "medium" : "low",
|
|
6926
7079
|
notes,
|
|
6927
|
-
|
|
6928
|
-
};
|
|
7080
|
+
pricingSource
|
|
7081
|
+
});
|
|
6929
7082
|
}
|
|
6930
7083
|
var AZURE_REDIS_PRICES = {
|
|
6931
7084
|
// Basic (single node, no SLA)
|
|
@@ -6978,20 +7131,16 @@ function calculateAzureRedisCacheCost(resource, targetProvider, targetRegion) {
|
|
|
6978
7131
|
`No pricing found for Azure Redis Cache ${sku} ${capacityStr.toUpperCase()}; cost reported as $0`
|
|
6979
7132
|
);
|
|
6980
7133
|
}
|
|
6981
|
-
return {
|
|
6982
|
-
|
|
6983
|
-
resource_type: resource.type,
|
|
6984
|
-
resource_name: resource.name,
|
|
7134
|
+
return makeCostEstimate({
|
|
7135
|
+
resource,
|
|
6985
7136
|
provider: targetProvider,
|
|
6986
7137
|
region: targetRegion,
|
|
6987
|
-
|
|
6988
|
-
yearly_cost: totalMonthly * 12,
|
|
6989
|
-
currency: "USD",
|
|
7138
|
+
monthlyCost: totalMonthly,
|
|
6990
7139
|
breakdown,
|
|
6991
7140
|
confidence: hourlyPrice !== void 0 ? "medium" : "low",
|
|
6992
7141
|
notes,
|
|
6993
|
-
|
|
6994
|
-
};
|
|
7142
|
+
pricingSource
|
|
7143
|
+
});
|
|
6995
7144
|
}
|
|
6996
7145
|
var GCP_REDIS_PRICES = {
|
|
6997
7146
|
basic: 0.049,
|
|
@@ -7015,20 +7164,16 @@ function calculateGcpRedisCost(resource, targetProvider, targetRegion) {
|
|
|
7015
7164
|
notes.push(
|
|
7016
7165
|
`GCP Memorystore Redis: ${normalizedTier} tier, ${memorySizeGb}GB \u2014 $${pricePerGbHour}/GB/hr`
|
|
7017
7166
|
);
|
|
7018
|
-
return {
|
|
7019
|
-
|
|
7020
|
-
resource_type: resource.type,
|
|
7021
|
-
resource_name: resource.name,
|
|
7167
|
+
return makeCostEstimate({
|
|
7168
|
+
resource,
|
|
7022
7169
|
provider: targetProvider,
|
|
7023
7170
|
region: targetRegion,
|
|
7024
|
-
|
|
7025
|
-
yearly_cost: totalMonthly * 12,
|
|
7026
|
-
currency: "USD",
|
|
7171
|
+
monthlyCost: totalMonthly,
|
|
7027
7172
|
breakdown,
|
|
7028
7173
|
confidence: "medium",
|
|
7029
7174
|
notes,
|
|
7030
|
-
|
|
7031
|
-
};
|
|
7175
|
+
pricingSource: "fallback"
|
|
7176
|
+
});
|
|
7032
7177
|
}
|
|
7033
7178
|
var CLOUDFRONT_DATA_FIRST_10TB_PER_GB = 0.085;
|
|
7034
7179
|
var DEFAULT_CLOUDFRONT_TRANSFER_GB = 100;
|
|
@@ -7068,20 +7213,16 @@ function calculateCloudFrontCost(resource, targetProvider, targetRegion) {
|
|
|
7068
7213
|
notes.push(
|
|
7069
7214
|
`CloudFront ${priceClass} \u2014 HTTPS request charges excluded (runtime traffic data required)`
|
|
7070
7215
|
);
|
|
7071
|
-
return {
|
|
7072
|
-
|
|
7073
|
-
resource_type: resource.type,
|
|
7074
|
-
resource_name: resource.name,
|
|
7216
|
+
return makeCostEstimate({
|
|
7217
|
+
resource,
|
|
7075
7218
|
provider: targetProvider,
|
|
7076
7219
|
region: targetRegion,
|
|
7077
|
-
|
|
7078
|
-
yearly_cost: totalMonthly * 12,
|
|
7079
|
-
currency: "USD",
|
|
7220
|
+
monthlyCost: totalMonthly,
|
|
7080
7221
|
breakdown,
|
|
7081
7222
|
confidence: "medium",
|
|
7082
7223
|
notes,
|
|
7083
|
-
|
|
7084
|
-
};
|
|
7224
|
+
pricingSource: "fallback"
|
|
7225
|
+
});
|
|
7085
7226
|
}
|
|
7086
7227
|
|
|
7087
7228
|
// src/calculator/paas.ts
|
|
@@ -7148,20 +7289,16 @@ function calculateAppServicePlanCost(resource, targetProvider, targetRegion) {
|
|
|
7148
7289
|
`No pricing found for App Service Plan SKU "${skuName || skuSize || skuTier}"; cost reported as $0`
|
|
7149
7290
|
);
|
|
7150
7291
|
}
|
|
7151
|
-
return {
|
|
7152
|
-
|
|
7153
|
-
resource_type: resource.type,
|
|
7154
|
-
resource_name: resource.name,
|
|
7292
|
+
return makeCostEstimate({
|
|
7293
|
+
resource,
|
|
7155
7294
|
provider: targetProvider,
|
|
7156
7295
|
region: targetRegion,
|
|
7157
|
-
|
|
7158
|
-
yearly_cost: totalMonthly * 12,
|
|
7159
|
-
currency: "USD",
|
|
7296
|
+
monthlyCost: totalMonthly,
|
|
7160
7297
|
breakdown,
|
|
7161
7298
|
confidence: hourlyPrice !== void 0 ? "medium" : "low",
|
|
7162
7299
|
notes,
|
|
7163
|
-
|
|
7164
|
-
};
|
|
7300
|
+
pricingSource
|
|
7301
|
+
});
|
|
7165
7302
|
}
|
|
7166
7303
|
var COSMOS_SERVERLESS_PER_MILLION_RU = 0.25;
|
|
7167
7304
|
var COSMOS_PROVISIONED_PER_100_RU_PER_HR = 8e-3;
|
|
@@ -7219,20 +7356,16 @@ function calculateCosmosDbCost(resource, targetProvider, targetRegion) {
|
|
|
7219
7356
|
});
|
|
7220
7357
|
totalMonthly += storageCost;
|
|
7221
7358
|
}
|
|
7222
|
-
return {
|
|
7223
|
-
|
|
7224
|
-
resource_type: resource.type,
|
|
7225
|
-
resource_name: resource.name,
|
|
7359
|
+
return makeCostEstimate({
|
|
7360
|
+
resource,
|
|
7226
7361
|
provider: targetProvider,
|
|
7227
7362
|
region: targetRegion,
|
|
7228
|
-
|
|
7229
|
-
yearly_cost: totalMonthly * 12,
|
|
7230
|
-
currency: "USD",
|
|
7363
|
+
monthlyCost: totalMonthly,
|
|
7231
7364
|
breakdown,
|
|
7232
7365
|
confidence: "medium",
|
|
7233
7366
|
notes,
|
|
7234
|
-
|
|
7235
|
-
};
|
|
7367
|
+
pricingSource
|
|
7368
|
+
});
|
|
7236
7369
|
}
|
|
7237
7370
|
var AZURE_FUNC_PER_MILLION_EXECUTIONS = 0.2;
|
|
7238
7371
|
var AZURE_FUNC_GB_SECOND_PRICE = 16e-6;
|
|
@@ -7269,20 +7402,16 @@ function calculateAzureFunctionCost(resource, targetProvider, targetRegion) {
|
|
|
7269
7402
|
);
|
|
7270
7403
|
}
|
|
7271
7404
|
notes.push("Azure Function App on Consumption plan (dedicated plan not priced separately)");
|
|
7272
|
-
return {
|
|
7273
|
-
|
|
7274
|
-
resource_type: resource.type,
|
|
7275
|
-
resource_name: resource.name,
|
|
7405
|
+
return makeCostEstimate({
|
|
7406
|
+
resource,
|
|
7276
7407
|
provider: targetProvider,
|
|
7277
7408
|
region: targetRegion,
|
|
7278
|
-
|
|
7279
|
-
yearly_cost: totalMonthly * 12,
|
|
7280
|
-
currency: "USD",
|
|
7409
|
+
monthlyCost: totalMonthly,
|
|
7281
7410
|
breakdown,
|
|
7282
7411
|
confidence: "medium",
|
|
7283
7412
|
notes,
|
|
7284
|
-
|
|
7285
|
-
};
|
|
7413
|
+
pricingSource: "fallback"
|
|
7414
|
+
});
|
|
7286
7415
|
}
|
|
7287
7416
|
var CLOUD_RUN_VCPU_PER_SECOND = 24e-6;
|
|
7288
7417
|
var CLOUD_RUN_MEMORY_GIB_PER_SECOND = 25e-7;
|
|
@@ -7355,20 +7484,16 @@ function calculateCloudRunCost(resource, targetProvider, targetRegion) {
|
|
|
7355
7484
|
);
|
|
7356
7485
|
}
|
|
7357
7486
|
notes.push(`Cloud Run: ${vcpus} vCPU, ${memoryGib.toFixed(2)}GiB memory`);
|
|
7358
|
-
return {
|
|
7359
|
-
|
|
7360
|
-
resource_type: resource.type,
|
|
7361
|
-
resource_name: resource.name,
|
|
7487
|
+
return makeCostEstimate({
|
|
7488
|
+
resource,
|
|
7362
7489
|
provider: targetProvider,
|
|
7363
7490
|
region: targetRegion,
|
|
7364
|
-
|
|
7365
|
-
yearly_cost: totalMonthly * 12,
|
|
7366
|
-
currency: "USD",
|
|
7491
|
+
monthlyCost: totalMonthly,
|
|
7367
7492
|
breakdown,
|
|
7368
7493
|
confidence: "medium",
|
|
7369
7494
|
notes,
|
|
7370
|
-
|
|
7371
|
-
};
|
|
7495
|
+
pricingSource: "fallback"
|
|
7496
|
+
});
|
|
7372
7497
|
}
|
|
7373
7498
|
var BIGQUERY_QUERY_PER_TB = 5;
|
|
7374
7499
|
var BIGQUERY_STORAGE_PER_GB = 0.02;
|
|
@@ -7401,24 +7526,20 @@ function calculateBigQueryCost(resource, targetProvider, targetRegion) {
|
|
|
7401
7526
|
`Default assumption: ${DEFAULT_BIGQUERY_QUERY_TB}TB queries/month \u2014 provide monthly_query_tb attribute for accuracy`
|
|
7402
7527
|
);
|
|
7403
7528
|
}
|
|
7404
|
-
if (resource.attributes.location) {
|
|
7529
|
+
if (typeof resource.attributes.location === "string" && resource.attributes.location) {
|
|
7405
7530
|
notes.push(`BigQuery location: ${resource.attributes.location}`);
|
|
7406
7531
|
}
|
|
7407
7532
|
notes.push("BigQuery on-demand pricing; flat-rate/reservations not included");
|
|
7408
|
-
return {
|
|
7409
|
-
|
|
7410
|
-
resource_type: resource.type,
|
|
7411
|
-
resource_name: resource.name,
|
|
7533
|
+
return makeCostEstimate({
|
|
7534
|
+
resource,
|
|
7412
7535
|
provider: targetProvider,
|
|
7413
7536
|
region: targetRegion,
|
|
7414
|
-
|
|
7415
|
-
yearly_cost: totalMonthly * 12,
|
|
7416
|
-
currency: "USD",
|
|
7537
|
+
monthlyCost: totalMonthly,
|
|
7417
7538
|
breakdown,
|
|
7418
7539
|
confidence: "medium",
|
|
7419
7540
|
notes,
|
|
7420
|
-
|
|
7421
|
-
};
|
|
7541
|
+
pricingSource: "fallback"
|
|
7542
|
+
});
|
|
7422
7543
|
}
|
|
7423
7544
|
var GCF_PER_MILLION_INVOCATIONS = 0.4;
|
|
7424
7545
|
var GCF_GB_SECOND_PRICE = 25e-7;
|
|
@@ -7454,114 +7575,38 @@ function calculateGcpFunctionCost(resource, targetProvider, targetRegion) {
|
|
|
7454
7575
|
`Default assumption: ${DEFAULT_GCF_INVOCATIONS.toLocaleString()} invocations/month, ${DEFAULT_GCF_DURATION_MS}ms avg, ${DEFAULT_GCF_MEMORY_MB}MB memory`
|
|
7455
7576
|
);
|
|
7456
7577
|
}
|
|
7457
|
-
if (resource.attributes.runtime) {
|
|
7578
|
+
if (typeof resource.attributes.runtime === "string" && resource.attributes.runtime) {
|
|
7458
7579
|
notes.push(`Cloud Function runtime: ${resource.attributes.runtime}`);
|
|
7459
7580
|
}
|
|
7460
|
-
return {
|
|
7461
|
-
|
|
7462
|
-
resource_type: resource.type,
|
|
7463
|
-
resource_name: resource.name,
|
|
7581
|
+
return makeCostEstimate({
|
|
7582
|
+
resource,
|
|
7464
7583
|
provider: targetProvider,
|
|
7465
7584
|
region: targetRegion,
|
|
7466
|
-
|
|
7467
|
-
yearly_cost: totalMonthly * 12,
|
|
7468
|
-
currency: "USD",
|
|
7585
|
+
monthlyCost: totalMonthly,
|
|
7469
7586
|
breakdown,
|
|
7470
7587
|
confidence: "medium",
|
|
7471
7588
|
notes,
|
|
7472
|
-
|
|
7473
|
-
};
|
|
7589
|
+
pricingSource: "fallback"
|
|
7590
|
+
});
|
|
7474
7591
|
}
|
|
7475
7592
|
|
|
7476
7593
|
// src/calculator/data-transfer.ts
|
|
7477
7594
|
var DEFAULT_EGRESS_GB = 100;
|
|
7478
|
-
var
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
|
|
7489
|
-
|
|
7490
|
-
|
|
7491
|
-
"ap-southeast-2": 1.11,
|
|
7492
|
-
"ap-northeast-1": 1.11,
|
|
7493
|
-
"ap-northeast-2": 1.11,
|
|
7494
|
-
"ap-south-1": 1.11,
|
|
7495
|
-
"sa-east-1": 1.31,
|
|
7496
|
-
// $0.118/GB
|
|
7497
|
-
"ca-central-1": 1,
|
|
7498
|
-
"eu-north-1": 1,
|
|
7499
|
-
"me-south-1": 1.11,
|
|
7500
|
-
"af-south-1": 1.16
|
|
7501
|
-
// $0.1046/GB
|
|
7502
|
-
};
|
|
7503
|
-
var AZURE_REGION_MULTIPLIERS = {
|
|
7504
|
-
eastus: 1,
|
|
7505
|
-
eastus2: 1,
|
|
7506
|
-
westus: 1,
|
|
7507
|
-
westus2: 1,
|
|
7508
|
-
westus3: 1,
|
|
7509
|
-
centralus: 1,
|
|
7510
|
-
northeurope: 1,
|
|
7511
|
-
westeurope: 1,
|
|
7512
|
-
uksouth: 1,
|
|
7513
|
-
ukwest: 1,
|
|
7514
|
-
germanywestcentral: 1,
|
|
7515
|
-
francecentral: 1,
|
|
7516
|
-
eastasia: 1.16,
|
|
7517
|
-
// Zone 2 rate
|
|
7518
|
-
southeastasia: 1.16,
|
|
7519
|
-
japaneast: 1.16,
|
|
7520
|
-
koreacentral: 1.16,
|
|
7521
|
-
australiaeast: 1.24,
|
|
7522
|
-
// Zone 3 rate
|
|
7523
|
-
brazilsouth: 1.31,
|
|
7524
|
-
southafricanorth: 1.31
|
|
7525
|
-
};
|
|
7526
|
-
var GCP_REGION_MULTIPLIERS = {
|
|
7527
|
-
"us-central1": 1,
|
|
7528
|
-
"us-east1": 1,
|
|
7529
|
-
"us-east4": 1,
|
|
7530
|
-
"us-west1": 1,
|
|
7531
|
-
"us-west2": 1,
|
|
7532
|
-
"us-west3": 1,
|
|
7533
|
-
"us-west4": 1,
|
|
7534
|
-
"europe-west1": 1,
|
|
7535
|
-
"europe-west2": 1,
|
|
7536
|
-
"europe-west3": 1,
|
|
7537
|
-
"europe-west4": 1,
|
|
7538
|
-
"europe-west6": 1,
|
|
7539
|
-
"europe-north1": 1,
|
|
7540
|
-
"northamerica-northeast1": 1,
|
|
7541
|
-
"southamerica-east1": 1.59,
|
|
7542
|
-
// $0.135/GB
|
|
7543
|
-
"asia-east1": 1.41,
|
|
7544
|
-
// $0.12/GB (APAC)
|
|
7545
|
-
"asia-east2": 1.41,
|
|
7546
|
-
"asia-northeast1": 1.41,
|
|
7547
|
-
"asia-northeast2": 1.41,
|
|
7548
|
-
"asia-northeast3": 1.41,
|
|
7549
|
-
"asia-south1": 1.41,
|
|
7550
|
-
"asia-southeast1": 1.41,
|
|
7551
|
-
"asia-southeast2": 1.41,
|
|
7552
|
-
"australia-southeast1": 1.41,
|
|
7553
|
-
"australia-southeast2": 1.41
|
|
7595
|
+
var EGRESS_PRICING = {
|
|
7596
|
+
aws: {
|
|
7597
|
+
ratePerGb: 0.09,
|
|
7598
|
+
note: "Data transfer costs cover internet egress only; intra-region and inter-AZ traffic charges are excluded from this estimate"
|
|
7599
|
+
},
|
|
7600
|
+
azure: {
|
|
7601
|
+
ratePerGb: 0.087,
|
|
7602
|
+
note: "Data transfer costs cover internet egress only; intra-region traffic is free on Azure and excluded from this estimate"
|
|
7603
|
+
},
|
|
7604
|
+
gcp: {
|
|
7605
|
+
ratePerGb: 0.085,
|
|
7606
|
+
note: "Data transfer costs cover internet egress only; intra-region and Google network traffic charges are excluded from this estimate"
|
|
7607
|
+
}
|
|
7554
7608
|
};
|
|
7555
|
-
function
|
|
7556
|
-
return AWS_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
|
|
7557
|
-
}
|
|
7558
|
-
function azureRegionMultiplier(region) {
|
|
7559
|
-
return AZURE_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
|
|
7560
|
-
}
|
|
7561
|
-
function gcpRegionMultiplier(region) {
|
|
7562
|
-
return GCP_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
|
|
7563
|
-
}
|
|
7564
|
-
async function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
7609
|
+
function calculateEgressCost(resource, provider, targetRegion) {
|
|
7565
7610
|
const notes = [];
|
|
7566
7611
|
const breakdown = [];
|
|
7567
7612
|
const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
|
|
@@ -7570,8 +7615,8 @@ async function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
|
7570
7615
|
`Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
|
|
7571
7616
|
);
|
|
7572
7617
|
}
|
|
7573
|
-
const
|
|
7574
|
-
const ratePerGb =
|
|
7618
|
+
const pricing = EGRESS_PRICING[provider];
|
|
7619
|
+
const ratePerGb = pricing.ratePerGb * regionMultiplier(provider, targetRegion);
|
|
7575
7620
|
const totalMonthly = ratePerGb * egressGb;
|
|
7576
7621
|
breakdown.push({
|
|
7577
7622
|
description: `Internet egress (${egressGb} GB/month, estimated)`,
|
|
@@ -7580,97 +7625,26 @@ async function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
|
7580
7625
|
unit_price: ratePerGb,
|
|
7581
7626
|
monthly_cost: totalMonthly
|
|
7582
7627
|
});
|
|
7583
|
-
notes.push(
|
|
7584
|
-
|
|
7585
|
-
|
|
7586
|
-
|
|
7587
|
-
resource_id: resource.id,
|
|
7588
|
-
resource_type: resource.type,
|
|
7589
|
-
resource_name: resource.name,
|
|
7590
|
-
provider: "aws",
|
|
7628
|
+
notes.push(pricing.note);
|
|
7629
|
+
return makeCostEstimate({
|
|
7630
|
+
resource,
|
|
7631
|
+
provider,
|
|
7591
7632
|
region: targetRegion,
|
|
7592
|
-
|
|
7593
|
-
yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
|
|
7594
|
-
currency: "USD",
|
|
7633
|
+
monthlyCost: totalMonthly,
|
|
7595
7634
|
breakdown,
|
|
7596
7635
|
confidence: "low",
|
|
7597
7636
|
notes,
|
|
7598
|
-
|
|
7599
|
-
};
|
|
7600
|
-
}
|
|
7601
|
-
async function calculateAzureDataTransferCost(resource, targetRegion) {
|
|
7602
|
-
const notes = [];
|
|
7603
|
-
const breakdown = [];
|
|
7604
|
-
const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
|
|
7605
|
-
if (!resource.attributes.monthly_egress_gb || resource.attributes.monthly_egress_gb <= 0) {
|
|
7606
|
-
notes.push(
|
|
7607
|
-
`Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
|
|
7608
|
-
);
|
|
7609
|
-
}
|
|
7610
|
-
const multiplier = azureRegionMultiplier(targetRegion);
|
|
7611
|
-
const ratePerGb = AZURE_EGRESS_PER_GB * multiplier;
|
|
7612
|
-
const totalMonthly = ratePerGb * egressGb;
|
|
7613
|
-
breakdown.push({
|
|
7614
|
-
description: `Internet egress (${egressGb} GB/month, estimated)`,
|
|
7615
|
-
unit: "GB",
|
|
7616
|
-
quantity: egressGb,
|
|
7617
|
-
unit_price: ratePerGb,
|
|
7618
|
-
monthly_cost: totalMonthly
|
|
7637
|
+
pricingSource: "fallback"
|
|
7619
7638
|
});
|
|
7620
|
-
notes.push(
|
|
7621
|
-
"Data transfer costs cover internet egress only; intra-region traffic is free on Azure and excluded from this estimate"
|
|
7622
|
-
);
|
|
7623
|
-
return {
|
|
7624
|
-
resource_id: resource.id,
|
|
7625
|
-
resource_type: resource.type,
|
|
7626
|
-
resource_name: resource.name,
|
|
7627
|
-
provider: "azure",
|
|
7628
|
-
region: targetRegion,
|
|
7629
|
-
monthly_cost: Math.round(totalMonthly * 100) / 100,
|
|
7630
|
-
yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
|
|
7631
|
-
currency: "USD",
|
|
7632
|
-
breakdown,
|
|
7633
|
-
confidence: "low",
|
|
7634
|
-
notes,
|
|
7635
|
-
pricing_source: "fallback"
|
|
7636
|
-
};
|
|
7637
7639
|
}
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
}
|
|
7647
|
-
const multiplier = gcpRegionMultiplier(targetRegion);
|
|
7648
|
-
const ratePerGb = GCP_EGRESS_PER_GB * multiplier;
|
|
7649
|
-
const totalMonthly = ratePerGb * egressGb;
|
|
7650
|
-
breakdown.push({
|
|
7651
|
-
description: `Internet egress (${egressGb} GB/month, estimated)`,
|
|
7652
|
-
unit: "GB",
|
|
7653
|
-
quantity: egressGb,
|
|
7654
|
-
unit_price: ratePerGb,
|
|
7655
|
-
monthly_cost: totalMonthly
|
|
7656
|
-
});
|
|
7657
|
-
notes.push(
|
|
7658
|
-
"Data transfer costs cover internet egress only; intra-region and Google network traffic charges are excluded from this estimate"
|
|
7659
|
-
);
|
|
7660
|
-
return {
|
|
7661
|
-
resource_id: resource.id,
|
|
7662
|
-
resource_type: resource.type,
|
|
7663
|
-
resource_name: resource.name,
|
|
7664
|
-
provider: "gcp",
|
|
7665
|
-
region: targetRegion,
|
|
7666
|
-
monthly_cost: Math.round(totalMonthly * 100) / 100,
|
|
7667
|
-
yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
|
|
7668
|
-
currency: "USD",
|
|
7669
|
-
breakdown,
|
|
7670
|
-
confidence: "low",
|
|
7671
|
-
notes,
|
|
7672
|
-
pricing_source: "fallback"
|
|
7673
|
-
};
|
|
7640
|
+
function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
7641
|
+
return calculateEgressCost(resource, "aws", targetRegion);
|
|
7642
|
+
}
|
|
7643
|
+
function calculateAzureDataTransferCost(resource, targetRegion) {
|
|
7644
|
+
return calculateEgressCost(resource, "azure", targetRegion);
|
|
7645
|
+
}
|
|
7646
|
+
function calculateGcpDataTransferCost(resource, targetRegion) {
|
|
7647
|
+
return calculateEgressCost(resource, "gcp", targetRegion);
|
|
7674
7648
|
}
|
|
7675
7649
|
|
|
7676
7650
|
// src/calculator/container-registry.ts
|
|
@@ -7721,20 +7695,16 @@ function calculateContainerRegistryCost(resource, targetProvider, targetRegion)
|
|
|
7721
7695
|
monthly_cost: storageCost
|
|
7722
7696
|
});
|
|
7723
7697
|
const totalMonthly = storageCost;
|
|
7724
|
-
return {
|
|
7725
|
-
|
|
7726
|
-
resource_type: resource.type,
|
|
7727
|
-
resource_name: resource.name,
|
|
7698
|
+
return makeCostEstimate({
|
|
7699
|
+
resource,
|
|
7728
7700
|
provider: targetProvider,
|
|
7729
7701
|
region: targetRegion,
|
|
7730
|
-
|
|
7731
|
-
yearly_cost: totalMonthly * 12,
|
|
7732
|
-
currency: "USD",
|
|
7702
|
+
monthlyCost: totalMonthly,
|
|
7733
7703
|
breakdown,
|
|
7734
7704
|
confidence: hasExplicitStorage ? "high" : "medium",
|
|
7735
7705
|
notes,
|
|
7736
|
-
|
|
7737
|
-
};
|
|
7706
|
+
pricingSource: "bundled"
|
|
7707
|
+
});
|
|
7738
7708
|
}
|
|
7739
7709
|
|
|
7740
7710
|
// src/calculator/secrets.ts
|
|
@@ -7829,20 +7799,16 @@ function calculateSecretsCost(resource, targetProvider, targetRegion) {
|
|
|
7829
7799
|
});
|
|
7830
7800
|
totalMonthly += accessOpFee;
|
|
7831
7801
|
}
|
|
7832
|
-
return {
|
|
7833
|
-
|
|
7834
|
-
resource_type: resource.type,
|
|
7835
|
-
resource_name: resource.name,
|
|
7802
|
+
return makeCostEstimate({
|
|
7803
|
+
resource,
|
|
7836
7804
|
provider: targetProvider,
|
|
7837
7805
|
region: targetRegion,
|
|
7838
|
-
|
|
7839
|
-
yearly_cost: totalMonthly * 12,
|
|
7840
|
-
currency: "USD",
|
|
7806
|
+
monthlyCost: totalMonthly,
|
|
7841
7807
|
breakdown,
|
|
7842
7808
|
confidence: hasExplicitSecretCount ? "high" : "medium",
|
|
7843
7809
|
notes,
|
|
7844
|
-
|
|
7845
|
-
};
|
|
7810
|
+
pricingSource: "bundled"
|
|
7811
|
+
});
|
|
7846
7812
|
}
|
|
7847
7813
|
|
|
7848
7814
|
// src/calculator/dns.ts
|
|
@@ -7896,20 +7862,16 @@ function calculateDnsCost(resource, targetProvider, targetRegion) {
|
|
|
7896
7862
|
monthly_cost: queryCost
|
|
7897
7863
|
});
|
|
7898
7864
|
const totalMonthly = zonePrice + queryCost;
|
|
7899
|
-
return {
|
|
7900
|
-
|
|
7901
|
-
resource_type: resource.type,
|
|
7902
|
-
resource_name: resource.name,
|
|
7865
|
+
return makeCostEstimate({
|
|
7866
|
+
resource,
|
|
7903
7867
|
provider: targetProvider,
|
|
7904
7868
|
region: targetRegion,
|
|
7905
|
-
|
|
7906
|
-
yearly_cost: totalMonthly * 12,
|
|
7907
|
-
currency: "USD",
|
|
7869
|
+
monthlyCost: totalMonthly,
|
|
7908
7870
|
breakdown,
|
|
7909
7871
|
confidence: hasExplicitQueries ? "high" : "medium",
|
|
7910
7872
|
notes,
|
|
7911
|
-
|
|
7912
|
-
};
|
|
7873
|
+
pricingSource: "bundled"
|
|
7874
|
+
});
|
|
7913
7875
|
}
|
|
7914
7876
|
|
|
7915
7877
|
// src/calculator/api-gateway.ts
|
|
@@ -8030,20 +7992,16 @@ function calculateApiGatewayCost(resource, targetProvider, targetRegion) {
|
|
|
8030
7992
|
);
|
|
8031
7993
|
}
|
|
8032
7994
|
}
|
|
8033
|
-
return {
|
|
8034
|
-
|
|
8035
|
-
resource_type: resource.type,
|
|
8036
|
-
resource_name: resource.name,
|
|
7995
|
+
return makeCostEstimate({
|
|
7996
|
+
resource,
|
|
8037
7997
|
provider: targetProvider,
|
|
8038
7998
|
region: targetRegion,
|
|
8039
|
-
|
|
8040
|
-
yearly_cost: totalMonthly * 12,
|
|
8041
|
-
currency: "USD",
|
|
7999
|
+
monthlyCost: totalMonthly,
|
|
8042
8000
|
breakdown,
|
|
8043
8001
|
confidence: hasExplicitRequests ? "high" : "medium",
|
|
8044
8002
|
notes,
|
|
8045
|
-
|
|
8046
|
-
};
|
|
8003
|
+
pricingSource: "fallback"
|
|
8004
|
+
});
|
|
8047
8005
|
}
|
|
8048
8006
|
|
|
8049
8007
|
// src/calculator/waf.ts
|
|
@@ -8157,20 +8115,16 @@ function calculateWafCost(resource, targetProvider, targetRegion) {
|
|
|
8157
8115
|
totalMonthly += requestCost;
|
|
8158
8116
|
notes.push("GCP equivalent: Cloud Armor security policy");
|
|
8159
8117
|
}
|
|
8160
|
-
return {
|
|
8161
|
-
|
|
8162
|
-
resource_type: resource.type,
|
|
8163
|
-
resource_name: resource.name,
|
|
8118
|
+
return makeCostEstimate({
|
|
8119
|
+
resource,
|
|
8164
8120
|
provider: targetProvider,
|
|
8165
8121
|
region: targetRegion,
|
|
8166
|
-
|
|
8167
|
-
yearly_cost: totalMonthly * 12,
|
|
8168
|
-
currency: "USD",
|
|
8122
|
+
monthlyCost: totalMonthly,
|
|
8169
8123
|
breakdown,
|
|
8170
8124
|
confidence: hasExplicitRules && hasExplicitRequests ? "high" : "medium",
|
|
8171
8125
|
notes,
|
|
8172
|
-
|
|
8173
|
-
};
|
|
8126
|
+
pricingSource: "fallback"
|
|
8127
|
+
});
|
|
8174
8128
|
}
|
|
8175
8129
|
|
|
8176
8130
|
// src/calculator/search.ts
|
|
@@ -8265,20 +8219,16 @@ function calculateSearchCost(resource, targetProvider, targetRegion) {
|
|
|
8265
8219
|
"GCP has no direct managed OpenSearch service; estimate based on comparable compute cost"
|
|
8266
8220
|
);
|
|
8267
8221
|
}
|
|
8268
|
-
return {
|
|
8269
|
-
|
|
8270
|
-
resource_type: resource.type,
|
|
8271
|
-
resource_name: resource.name,
|
|
8222
|
+
return makeCostEstimate({
|
|
8223
|
+
resource,
|
|
8272
8224
|
provider: targetProvider,
|
|
8273
8225
|
region: targetRegion,
|
|
8274
|
-
|
|
8275
|
-
yearly_cost: totalMonthly * 12,
|
|
8276
|
-
currency: "USD",
|
|
8226
|
+
monthlyCost: totalMonthly,
|
|
8277
8227
|
breakdown,
|
|
8278
8228
|
confidence: targetProvider === "aws" ? "medium" : "low",
|
|
8279
8229
|
notes,
|
|
8280
|
-
|
|
8281
|
-
};
|
|
8230
|
+
pricingSource: "fallback"
|
|
8231
|
+
});
|
|
8282
8232
|
}
|
|
8283
8233
|
|
|
8284
8234
|
// src/calculator/messaging.ts
|
|
@@ -8411,20 +8361,16 @@ function calculateMessagingCost(resource, targetProvider, targetRegion) {
|
|
|
8411
8361
|
);
|
|
8412
8362
|
}
|
|
8413
8363
|
}
|
|
8414
|
-
return {
|
|
8415
|
-
|
|
8416
|
-
resource_type: resource.type,
|
|
8417
|
-
resource_name: resource.name,
|
|
8364
|
+
return makeCostEstimate({
|
|
8365
|
+
resource,
|
|
8418
8366
|
provider: targetProvider,
|
|
8419
8367
|
region: targetRegion,
|
|
8420
|
-
|
|
8421
|
-
yearly_cost: totalMonthly * 12,
|
|
8422
|
-
currency: "USD",
|
|
8368
|
+
monthlyCost: totalMonthly,
|
|
8423
8369
|
breakdown,
|
|
8424
8370
|
confidence: hasExplicitMessages ? "high" : "medium",
|
|
8425
8371
|
notes,
|
|
8426
|
-
|
|
8427
|
-
};
|
|
8372
|
+
pricingSource: "fallback"
|
|
8373
|
+
});
|
|
8428
8374
|
}
|
|
8429
8375
|
function calculateMqBrokerCost(resource, targetProvider, targetRegion) {
|
|
8430
8376
|
const notes = [];
|
|
@@ -8461,20 +8407,16 @@ function calculateMqBrokerCost(resource, targetProvider, targetRegion) {
|
|
|
8461
8407
|
unit_price: AWS_MQ_STORAGE_PER_GB,
|
|
8462
8408
|
monthly_cost: storageCost
|
|
8463
8409
|
});
|
|
8464
|
-
return {
|
|
8465
|
-
|
|
8466
|
-
resource_type: resource.type,
|
|
8467
|
-
resource_name: resource.name,
|
|
8410
|
+
return makeCostEstimate({
|
|
8411
|
+
resource,
|
|
8468
8412
|
provider: targetProvider,
|
|
8469
8413
|
region: targetRegion,
|
|
8470
|
-
|
|
8471
|
-
yearly_cost: totalMonthly * 12,
|
|
8472
|
-
currency: "USD",
|
|
8414
|
+
monthlyCost: totalMonthly,
|
|
8473
8415
|
breakdown,
|
|
8474
8416
|
confidence: hourlyPrice !== void 0 ? "medium" : "low",
|
|
8475
8417
|
notes,
|
|
8476
|
-
|
|
8477
|
-
};
|
|
8418
|
+
pricingSource: "fallback"
|
|
8419
|
+
});
|
|
8478
8420
|
}
|
|
8479
8421
|
function calculateEventHubCost(resource, targetProvider, targetRegion) {
|
|
8480
8422
|
const notes = [];
|
|
@@ -8494,20 +8436,16 @@ function calculateEventHubCost(resource, targetProvider, targetRegion) {
|
|
|
8494
8436
|
notes.push(
|
|
8495
8437
|
`Azure Event Hubs: ${sku} tier, ${capacity} ${unitLabel}(s) \u2014 $${hourlyPrice}/${unitLabel}/hr`
|
|
8496
8438
|
);
|
|
8497
|
-
return {
|
|
8498
|
-
|
|
8499
|
-
resource_type: resource.type,
|
|
8500
|
-
resource_name: resource.name,
|
|
8439
|
+
return makeCostEstimate({
|
|
8440
|
+
resource,
|
|
8501
8441
|
provider: targetProvider,
|
|
8502
8442
|
region: targetRegion,
|
|
8503
|
-
|
|
8504
|
-
yearly_cost: totalMonthly * 12,
|
|
8505
|
-
currency: "USD",
|
|
8443
|
+
monthlyCost: totalMonthly,
|
|
8506
8444
|
breakdown,
|
|
8507
8445
|
confidence: "medium",
|
|
8508
8446
|
notes,
|
|
8509
|
-
|
|
8510
|
-
};
|
|
8447
|
+
pricingSource: "fallback"
|
|
8448
|
+
});
|
|
8511
8449
|
}
|
|
8512
8450
|
function calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion) {
|
|
8513
8451
|
const notes = [];
|
|
@@ -8530,20 +8468,16 @@ function calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion)
|
|
|
8530
8468
|
notes.push(
|
|
8531
8469
|
`GCP Pub/Sub subscription: $${GCP_PUBSUB_SUBSCRIPTION_PER_MILLION_OPS}/million ack operations`
|
|
8532
8470
|
);
|
|
8533
|
-
return {
|
|
8534
|
-
|
|
8535
|
-
resource_type: resource.type,
|
|
8536
|
-
resource_name: resource.name,
|
|
8471
|
+
return makeCostEstimate({
|
|
8472
|
+
resource,
|
|
8537
8473
|
provider: targetProvider,
|
|
8538
8474
|
region: targetRegion,
|
|
8539
|
-
|
|
8540
|
-
yearly_cost: opsCost * 12,
|
|
8541
|
-
currency: "USD",
|
|
8475
|
+
monthlyCost: opsCost,
|
|
8542
8476
|
breakdown,
|
|
8543
8477
|
confidence: hasExplicitMessages ? "high" : "medium",
|
|
8544
8478
|
notes,
|
|
8545
|
-
|
|
8546
|
-
};
|
|
8479
|
+
pricingSource: "fallback"
|
|
8480
|
+
});
|
|
8547
8481
|
}
|
|
8548
8482
|
|
|
8549
8483
|
// src/calculator/ml-ai.ts
|
|
@@ -8661,20 +8595,16 @@ function calculateMlAiCost(resource, targetProvider, targetRegion) {
|
|
|
8661
8595
|
"Azure ML endpoint pricing not yet supported \u2014 consider Azure Machine Learning compute instance costs"
|
|
8662
8596
|
);
|
|
8663
8597
|
}
|
|
8664
|
-
return {
|
|
8665
|
-
|
|
8666
|
-
resource_type: resource.type,
|
|
8667
|
-
resource_name: resource.name,
|
|
8598
|
+
return makeCostEstimate({
|
|
8599
|
+
resource,
|
|
8668
8600
|
provider: targetProvider,
|
|
8669
8601
|
region: targetRegion,
|
|
8670
|
-
|
|
8671
|
-
yearly_cost: totalMonthly * 12,
|
|
8672
|
-
currency: "USD",
|
|
8602
|
+
monthlyCost: totalMonthly,
|
|
8673
8603
|
breakdown,
|
|
8674
8604
|
confidence: "low",
|
|
8675
8605
|
notes,
|
|
8676
|
-
|
|
8677
|
-
};
|
|
8606
|
+
pricingSource: "fallback"
|
|
8607
|
+
});
|
|
8678
8608
|
}
|
|
8679
8609
|
|
|
8680
8610
|
// src/calculator/cost-engine.ts
|
|
@@ -8971,20 +8901,16 @@ var CostEngine = class {
|
|
|
8971
8901
|
return calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion);
|
|
8972
8902
|
}
|
|
8973
8903
|
logger.warn("CostEngine: unsupported resource type", { type, targetProvider });
|
|
8974
|
-
return {
|
|
8975
|
-
|
|
8976
|
-
resource_type: type,
|
|
8977
|
-
resource_name: resource.name,
|
|
8904
|
+
return makeCostEstimate({
|
|
8905
|
+
resource,
|
|
8978
8906
|
provider: targetProvider,
|
|
8979
8907
|
region: targetRegion,
|
|
8980
|
-
|
|
8981
|
-
yearly_cost: 0,
|
|
8982
|
-
currency: "USD",
|
|
8908
|
+
monthlyCost: 0,
|
|
8983
8909
|
breakdown: [],
|
|
8984
8910
|
confidence: "low",
|
|
8985
8911
|
notes: [`Resource type "${type}" is not yet supported by the cost engine`],
|
|
8986
|
-
|
|
8987
|
-
};
|
|
8912
|
+
pricingSource: "fallback"
|
|
8913
|
+
});
|
|
8988
8914
|
}
|
|
8989
8915
|
/**
|
|
8990
8916
|
* Calculates costs for all resources and aggregates them into a CostBreakdown.
|
|
@@ -9050,7 +8976,7 @@ var CostEngine = class {
|
|
|
9050
8976
|
name: `data-transfer-${estimate.provider}-${estimate.region}`,
|
|
9051
8977
|
provider: estimate.provider,
|
|
9052
8978
|
region: estimate.region,
|
|
9053
|
-
attributes: { monthly_egress_gb:
|
|
8979
|
+
attributes: { monthly_egress_gb: DEFAULT_EGRESS_GB },
|
|
9054
8980
|
tags: {},
|
|
9055
8981
|
source_file: "synthetic"
|
|
9056
8982
|
}
|
|
@@ -9078,6 +9004,12 @@ var CostEngine = class {
|
|
|
9078
9004
|
}
|
|
9079
9005
|
}
|
|
9080
9006
|
const totalMonthly = estimates.reduce((sum, e) => sum + e.monthly_cost, 0);
|
|
9007
|
+
const estimatedEgressMonthly = estimates.filter((e) => DATA_TRANSFER_TYPES.has(e.resource_type)).reduce((sum, e) => sum + e.monthly_cost, 0);
|
|
9008
|
+
if (estimatedEgressMonthly > 0) {
|
|
9009
|
+
warnings.push(
|
|
9010
|
+
`total_monthly includes a synthetic estimate of ${DEFAULT_EGRESS_GB} GB/month internet egress ($${(Math.round(estimatedEgressMonthly * 100) / 100).toFixed(2)}/mo, USD) \u2014 see estimated_egress_monthly. Set CLOUDCOST_INCLUDE_DATA_TRANSFER=false to exclude it.`
|
|
9011
|
+
);
|
|
9012
|
+
}
|
|
9081
9013
|
const budgetWarnings = this.evaluateBudget(totalMonthly, estimates);
|
|
9082
9014
|
return {
|
|
9083
9015
|
provider: targetProvider,
|
|
@@ -9091,6 +9023,9 @@ var CostEngine = class {
|
|
|
9091
9023
|
by_resource: estimates,
|
|
9092
9024
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
9093
9025
|
warnings,
|
|
9026
|
+
...estimatedEgressMonthly > 0 && {
|
|
9027
|
+
estimated_egress_monthly: Math.round(estimatedEgressMonthly * 100) / 100
|
|
9028
|
+
},
|
|
9094
9029
|
...budgetWarnings.length > 0 && { budget_warnings: budgetWarnings }
|
|
9095
9030
|
};
|
|
9096
9031
|
}
|
|
@@ -9135,84 +9070,15 @@ var CostEngine = class {
|
|
|
9135
9070
|
}
|
|
9136
9071
|
};
|
|
9137
9072
|
|
|
9138
|
-
// src/currency.ts
|
|
9139
|
-
var SUPPORTED_CURRENCIES = [
|
|
9140
|
-
"USD",
|
|
9141
|
-
"EUR",
|
|
9142
|
-
"GBP",
|
|
9143
|
-
"JPY",
|
|
9144
|
-
"CAD",
|
|
9145
|
-
"AUD",
|
|
9146
|
-
"INR",
|
|
9147
|
-
"BRL"
|
|
9148
|
-
];
|
|
9149
|
-
var EXCHANGE_RATES = {
|
|
9150
|
-
USD: 1,
|
|
9151
|
-
EUR: 0.92,
|
|
9152
|
-
GBP: 0.79,
|
|
9153
|
-
JPY: 149.5,
|
|
9154
|
-
CAD: 1.36,
|
|
9155
|
-
AUD: 1.53,
|
|
9156
|
-
INR: 83.1,
|
|
9157
|
-
BRL: 4.97
|
|
9158
|
-
};
|
|
9159
|
-
function convertCurrency(amountUsd, targetCurrency) {
|
|
9160
|
-
const rate = EXCHANGE_RATES[targetCurrency];
|
|
9161
|
-
if (rate === void 0 || rate === 1) {
|
|
9162
|
-
return amountUsd;
|
|
9163
|
-
}
|
|
9164
|
-
return amountUsd * rate;
|
|
9165
|
-
}
|
|
9166
|
-
function convertLineItem(item, currency) {
|
|
9167
|
-
return {
|
|
9168
|
-
...item,
|
|
9169
|
-
unit_price: convertCurrency(item.unit_price, currency),
|
|
9170
|
-
monthly_cost: convertCurrency(item.monthly_cost, currency)
|
|
9171
|
-
};
|
|
9172
|
-
}
|
|
9173
|
-
function convertCostEstimate(estimate, currency) {
|
|
9174
|
-
return {
|
|
9175
|
-
...estimate,
|
|
9176
|
-
monthly_cost: convertCurrency(estimate.monthly_cost, currency),
|
|
9177
|
-
yearly_cost: convertCurrency(estimate.yearly_cost, currency),
|
|
9178
|
-
currency,
|
|
9179
|
-
breakdown: estimate.breakdown.map((item) => convertLineItem(item, currency))
|
|
9180
|
-
};
|
|
9181
|
-
}
|
|
9182
|
-
function convertBreakdownCurrency(breakdown, currency) {
|
|
9183
|
-
const original_usd = {
|
|
9184
|
-
total_monthly: breakdown.total_monthly,
|
|
9185
|
-
total_yearly: breakdown.total_yearly
|
|
9186
|
-
};
|
|
9187
|
-
const convertedByService = {};
|
|
9188
|
-
for (const [service, amount] of Object.entries(breakdown.by_service)) {
|
|
9189
|
-
convertedByService[service] = convertCurrency(amount, currency);
|
|
9190
|
-
}
|
|
9191
|
-
return {
|
|
9192
|
-
...breakdown,
|
|
9193
|
-
total_monthly: convertCurrency(breakdown.total_monthly, currency),
|
|
9194
|
-
total_yearly: convertCurrency(breakdown.total_yearly, currency),
|
|
9195
|
-
currency,
|
|
9196
|
-
by_service: convertedByService,
|
|
9197
|
-
by_resource: breakdown.by_resource.map((est) => convertCostEstimate(est, currency)),
|
|
9198
|
-
original_usd
|
|
9199
|
-
};
|
|
9200
|
-
}
|
|
9201
|
-
|
|
9202
9073
|
// src/tools/estimate-cost.ts
|
|
9203
|
-
var estimateCostSchema =
|
|
9204
|
-
files:
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
})
|
|
9209
|
-
).max(2e3, "files array exceeds 2000 entries"),
|
|
9210
|
-
tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
|
|
9211
|
-
provider: z3.enum(["aws", "azure", "gcp"]).describe("Target cloud provider to estimate costs for"),
|
|
9212
|
-
region: z3.string().optional().describe(
|
|
9074
|
+
var estimateCostSchema = z4.object({
|
|
9075
|
+
files: iacFilesSchema,
|
|
9076
|
+
tfvars: tfvarsField,
|
|
9077
|
+
provider: providerEnum.describe("Target cloud provider to estimate costs for"),
|
|
9078
|
+
region: shortStringSchema.optional().describe(
|
|
9213
9079
|
"Target region for pricing lookup. Defaults to the source region mapped to the target provider."
|
|
9214
9080
|
),
|
|
9215
|
-
currency:
|
|
9081
|
+
currency: currencyField
|
|
9216
9082
|
});
|
|
9217
9083
|
async function estimateCost(params, pricingEngine, config) {
|
|
9218
9084
|
const inventory = await parseTerraform(params.files, params.tfvars);
|
|
@@ -9249,12 +9115,13 @@ async function estimateCost(params, pricingEngine, config) {
|
|
|
9249
9115
|
return {
|
|
9250
9116
|
...convertedRest,
|
|
9251
9117
|
by_resource: byResource,
|
|
9252
|
-
warnings: allWarnings
|
|
9118
|
+
warnings: allWarnings,
|
|
9119
|
+
pricing_metadata: getPricingMetadata(targetProvider)
|
|
9253
9120
|
};
|
|
9254
9121
|
}
|
|
9255
9122
|
|
|
9256
9123
|
// src/tools/compare-providers.ts
|
|
9257
|
-
import { z as
|
|
9124
|
+
import { z as z5 } from "zod";
|
|
9258
9125
|
|
|
9259
9126
|
// src/calculator/reserved.ts
|
|
9260
9127
|
var DISCOUNT_RATES = {
|
|
@@ -9802,15 +9669,23 @@ function generateJsonReport(comparison, resources, monthlyHours = 730, parseWarn
|
|
|
9802
9669
|
return JSON.stringify(output);
|
|
9803
9670
|
}
|
|
9804
9671
|
|
|
9805
|
-
// src/reporting/csv-
|
|
9806
|
-
var PROVIDERS = ["aws", "azure", "gcp"];
|
|
9672
|
+
// src/reporting/csv-escape.ts
|
|
9807
9673
|
function csvEscape(value) {
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9674
|
+
if (typeof value === "number") {
|
|
9675
|
+
return Number.isFinite(value) ? String(value) : "";
|
|
9676
|
+
}
|
|
9677
|
+
let str2 = String(value);
|
|
9678
|
+
if (/^[=+\-@\t\r]/.test(str2)) {
|
|
9679
|
+
str2 = "'" + str2;
|
|
9680
|
+
}
|
|
9681
|
+
if (/["\n\r,]/.test(str2)) {
|
|
9682
|
+
str2 = `"${str2.replace(/"/g, '""')}"`;
|
|
9811
9683
|
}
|
|
9812
9684
|
return str2;
|
|
9813
9685
|
}
|
|
9686
|
+
|
|
9687
|
+
// src/reporting/csv-report.ts
|
|
9688
|
+
var PROVIDERS = ["aws", "azure", "gcp"];
|
|
9814
9689
|
function generateCsvReport(comparison, resources, options = {}) {
|
|
9815
9690
|
const lines = [];
|
|
9816
9691
|
const tagKey = options.group_by === "tag" ? options.group_by_tag_key : void 0;
|
|
@@ -9971,15 +9846,8 @@ function billingPeriodEnd() {
|
|
|
9971
9846
|
const now = /* @__PURE__ */ new Date();
|
|
9972
9847
|
return new Date(now.getFullYear(), now.getMonth() + 1, 1).toISOString().split("T")[0];
|
|
9973
9848
|
}
|
|
9974
|
-
function csvEscape2(value) {
|
|
9975
|
-
const str2 = String(value);
|
|
9976
|
-
if (str2.includes(",") || str2.includes("\n") || str2.includes('"')) {
|
|
9977
|
-
return `"${str2.replace(/"/g, '""')}"`;
|
|
9978
|
-
}
|
|
9979
|
-
return str2;
|
|
9980
|
-
}
|
|
9981
9849
|
function buildRow(values) {
|
|
9982
|
-
return values.map(
|
|
9850
|
+
return values.map(csvEscape).join(",");
|
|
9983
9851
|
}
|
|
9984
9852
|
function generateFocusReport(comparison, resources) {
|
|
9985
9853
|
const lines = [];
|
|
@@ -10046,17 +9914,12 @@ function generateFocusReport(comparison, resources) {
|
|
|
10046
9914
|
}
|
|
10047
9915
|
|
|
10048
9916
|
// src/tools/compare-providers.ts
|
|
10049
|
-
var compareProvidersSchema =
|
|
10050
|
-
files:
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10055
|
-
).max(2e3, "files array exceeds 2000 entries"),
|
|
10056
|
-
tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
|
|
10057
|
-
format: z4.enum(["markdown", "json", "csv", "focus"]).default("markdown").describe("Output report format"),
|
|
10058
|
-
providers: z4.array(z4.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison"),
|
|
10059
|
-
currency: z4.enum(SUPPORTED_CURRENCIES).optional().default("USD").describe("Output currency for cost estimates. Defaults to USD.")
|
|
9917
|
+
var compareProvidersSchema = z5.object({
|
|
9918
|
+
files: iacFilesSchema,
|
|
9919
|
+
tfvars: tfvarsField,
|
|
9920
|
+
format: z5.enum(["markdown", "json", "csv", "focus"]).default("markdown").describe("Output report format"),
|
|
9921
|
+
providers: z5.array(providerEnum).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison"),
|
|
9922
|
+
currency: currencyField
|
|
10060
9923
|
});
|
|
10061
9924
|
async function compareProviders(params, pricingEngine, config) {
|
|
10062
9925
|
const inventory = await parseTerraform(params.files, params.tfvars);
|
|
@@ -10112,11 +9975,13 @@ async function compareProviders(params, pricingEngine, config) {
|
|
|
10112
9975
|
break;
|
|
10113
9976
|
}
|
|
10114
9977
|
const dedupedWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allWarnings])];
|
|
9978
|
+
const pricingMetadata = Object.fromEntries(providerList.map((p) => [p, getPricingMetadata(p)]));
|
|
10115
9979
|
if (params.format === "json") {
|
|
10116
9980
|
return {
|
|
10117
9981
|
report,
|
|
10118
9982
|
format: params.format,
|
|
10119
|
-
warnings: dedupedWarnings
|
|
9983
|
+
warnings: dedupedWarnings,
|
|
9984
|
+
pricing_metadata: pricingMetadata
|
|
10120
9985
|
};
|
|
10121
9986
|
}
|
|
10122
9987
|
const comparisonSummary = {
|
|
@@ -10133,21 +9998,17 @@ async function compareProviders(params, pricingEngine, config) {
|
|
|
10133
9998
|
report,
|
|
10134
9999
|
format: params.format,
|
|
10135
10000
|
comparison: comparisonSummary,
|
|
10136
|
-
warnings: dedupedWarnings
|
|
10001
|
+
warnings: dedupedWarnings,
|
|
10002
|
+
pricing_metadata: pricingMetadata
|
|
10137
10003
|
};
|
|
10138
10004
|
}
|
|
10139
10005
|
|
|
10140
10006
|
// src/tools/optimize-cost.ts
|
|
10141
|
-
import { z as
|
|
10142
|
-
var optimizeCostSchema =
|
|
10143
|
-
files:
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
content: fileContentSchema.describe("File content (HCL)")
|
|
10147
|
-
})
|
|
10148
|
-
).max(2e3, "files array exceeds 2000 entries"),
|
|
10149
|
-
tfvars: tfvarsSchema.optional().describe("Contents of terraform.tfvars file"),
|
|
10150
|
-
providers: z5.array(z5.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Providers to calculate costs against for cross-provider recommendations")
|
|
10007
|
+
import { z as z6 } from "zod";
|
|
10008
|
+
var optimizeCostSchema = z6.object({
|
|
10009
|
+
files: iacFilesSchema,
|
|
10010
|
+
tfvars: tfvarsField,
|
|
10011
|
+
providers: z6.array(providerEnum).default(["aws", "azure", "gcp"]).describe("Providers to calculate costs against for cross-provider recommendations")
|
|
10151
10012
|
});
|
|
10152
10013
|
async function optimizeCost(params, pricingEngine, config) {
|
|
10153
10014
|
const inventory = await parseTerraform(params.files, params.tfvars);
|
|
@@ -10183,28 +10044,23 @@ async function optimizeCost(params, pricingEngine, config) {
|
|
|
10183
10044
|
}
|
|
10184
10045
|
|
|
10185
10046
|
// src/tools/what-if.ts
|
|
10186
|
-
import { z as
|
|
10187
|
-
var whatIfSchema =
|
|
10188
|
-
files:
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
content: fileContentSchema.describe("File content (HCL)")
|
|
10192
|
-
})
|
|
10193
|
-
).max(2e3, "files array exceeds 2000 entries").describe("Terraform HCL files to analyse"),
|
|
10194
|
-
changes: z6.array(
|
|
10195
|
-
z6.object({
|
|
10047
|
+
import { z as z7 } from "zod";
|
|
10048
|
+
var whatIfSchema = z7.object({
|
|
10049
|
+
files: iacFilesSchema.describe("Terraform HCL files to analyse"),
|
|
10050
|
+
changes: z7.array(
|
|
10051
|
+
z7.object({
|
|
10196
10052
|
resource_id: shortStringSchema.describe(
|
|
10197
10053
|
"The resource ID to modify (e.g. aws_instance.web)"
|
|
10198
10054
|
),
|
|
10199
10055
|
attribute: shortStringSchema.describe(
|
|
10200
10056
|
"The attribute name to change (e.g. instance_type, storage_size_gb)"
|
|
10201
10057
|
),
|
|
10202
|
-
new_value:
|
|
10058
|
+
new_value: z7.union([shortStringSchema, z7.number()]).describe("The new value for the attribute")
|
|
10203
10059
|
})
|
|
10204
10060
|
).max(200, "changes array exceeds 200 entries").describe("List of attribute changes to simulate"),
|
|
10205
|
-
provider:
|
|
10206
|
-
region:
|
|
10207
|
-
tfvars:
|
|
10061
|
+
provider: providerEnum.optional().describe("Target cloud provider. Defaults to auto-detecting from the files"),
|
|
10062
|
+
region: shortStringSchema.optional().describe("Target region for pricing. Defaults to the region detected from provider blocks"),
|
|
10063
|
+
tfvars: tfvarsSchema.optional().describe("Contents of a terraform.tfvars file for variable resolution")
|
|
10208
10064
|
});
|
|
10209
10065
|
function cloneResources(resources) {
|
|
10210
10066
|
return JSON.parse(JSON.stringify(resources));
|
|
@@ -10300,30 +10156,34 @@ async function whatIf(params, pricingEngine, config) {
|
|
|
10300
10156
|
}
|
|
10301
10157
|
|
|
10302
10158
|
export {
|
|
10303
|
-
loadConfig,
|
|
10304
10159
|
setLogLevel,
|
|
10305
10160
|
logger,
|
|
10161
|
+
loadConfig,
|
|
10162
|
+
SUPPORTED_CURRENCIES,
|
|
10163
|
+
convertCurrency,
|
|
10164
|
+
getExchangeRateInfo,
|
|
10165
|
+
convertBreakdownCurrency,
|
|
10306
10166
|
PricingCache,
|
|
10307
10167
|
PricingEngine,
|
|
10308
10168
|
sanitizeForMessage,
|
|
10169
|
+
coerceToString,
|
|
10309
10170
|
str,
|
|
10310
10171
|
num,
|
|
10311
10172
|
bool,
|
|
10312
10173
|
safeJsonParse,
|
|
10313
10174
|
parseTerraform,
|
|
10314
|
-
filePathSchema,
|
|
10315
|
-
fileContentSchema,
|
|
10316
|
-
tfvarsSchema,
|
|
10317
10175
|
planJsonSchema,
|
|
10318
10176
|
stateJsonSchema,
|
|
10177
|
+
shortStringSchema,
|
|
10178
|
+
providerEnum,
|
|
10179
|
+
iacFilesSchema,
|
|
10180
|
+
tfvarsField,
|
|
10181
|
+
currencyField,
|
|
10319
10182
|
analyzeTerraformSchema,
|
|
10320
10183
|
analyzeTerraform,
|
|
10321
10184
|
mapRegion,
|
|
10322
10185
|
mapInstance,
|
|
10323
10186
|
CostEngine,
|
|
10324
|
-
SUPPORTED_CURRENCIES,
|
|
10325
|
-
convertCurrency,
|
|
10326
|
-
convertBreakdownCurrency,
|
|
10327
10187
|
estimateCostSchema,
|
|
10328
10188
|
estimateCost,
|
|
10329
10189
|
compareProvidersSchema,
|
|
@@ -10333,4 +10193,4 @@ export {
|
|
|
10333
10193
|
whatIfSchema,
|
|
10334
10194
|
whatIf
|
|
10335
10195
|
};
|
|
10336
|
-
//# sourceMappingURL=chunk-
|
|
10196
|
+
//# sourceMappingURL=chunk-CGMU4ETN.js.map
|