@jadenrazo/cloudcost-mcp 1.0.0 → 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 +56 -5
- package/README.md +70 -115
- package/VERSIONING.md +99 -0
- package/data/aws-pricing/metadata.json +8 -0
- package/data/azure-pricing/metadata.json +8 -0
- package/data/gcp-pricing/metadata.json +4 -2
- package/dist/{chunk-E7KOWAMW.js → chunk-CGMU4ETN.js} +2010 -1209
- package/dist/{chunk-TRRAOOVF.js → chunk-LWTVE7UY.js} +31 -1
- package/dist/cli.d.ts +41 -0
- package/dist/cli.js +63 -22
- package/dist/index.js +391 -145
- package/dist/{loader-VXYJYDIH.js → loader-CMBV7346.js} +6 -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,87 +674,131 @@ function extractFromTerms(onDemandTerms, defaultUnit) {
|
|
|
562
674
|
}
|
|
563
675
|
return { price: 0, unit: defaultUnit };
|
|
564
676
|
}
|
|
565
|
-
function
|
|
566
|
-
const attrs = rawProduct?.attributes ?? {};
|
|
677
|
+
function normalize(rawPrice, region, effectiveDate, spec) {
|
|
567
678
|
const terms = rawPrice?.terms?.OnDemand ?? {};
|
|
568
|
-
const { price, unit } = extractFromTerms(terms,
|
|
679
|
+
const { price, unit } = extractFromTerms(terms, spec.defaultUnit);
|
|
569
680
|
return {
|
|
570
681
|
provider: "aws",
|
|
571
|
-
service:
|
|
572
|
-
resource_type:
|
|
682
|
+
service: spec.service,
|
|
683
|
+
resource_type: spec.resourceType,
|
|
573
684
|
region,
|
|
574
685
|
unit,
|
|
575
686
|
price_per_unit: price,
|
|
576
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",
|
|
577
702
|
description: attrs.instanceType ? `AWS EC2 ${attrs.instanceType} (${attrs.operatingSystem ?? "Linux"})` : void 0,
|
|
578
703
|
attributes: {
|
|
579
704
|
instance_type: attrs.instanceType ?? "",
|
|
580
705
|
vcpu: attrs.vcpu ?? "",
|
|
581
706
|
memory: attrs.memory ?? "",
|
|
582
707
|
operating_system: attrs.operatingSystem ?? "Linux",
|
|
583
|
-
tenancy: attrs.tenancy ?? "Shared"
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
587
|
-
};
|
|
708
|
+
tenancy: attrs.tenancy ?? "Shared"
|
|
709
|
+
}
|
|
710
|
+
});
|
|
588
711
|
}
|
|
589
|
-
function normalizeAwsDatabase(rawProduct, rawPrice, region) {
|
|
712
|
+
function normalizeAwsDatabase(rawProduct, rawPrice, region, effectiveDate) {
|
|
590
713
|
const attrs = rawProduct?.attributes ?? {};
|
|
591
|
-
|
|
592
|
-
const { price, unit } = extractFromTerms(terms, "Hrs");
|
|
593
|
-
return {
|
|
594
|
-
provider: "aws",
|
|
714
|
+
return normalize(rawPrice, region, effectiveDate, {
|
|
595
715
|
service: "rds",
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
unit,
|
|
599
|
-
price_per_unit: price,
|
|
600
|
-
currency: "USD",
|
|
716
|
+
defaultUnit: "Hrs",
|
|
717
|
+
resourceType: attrs.instanceType ?? "unknown",
|
|
601
718
|
description: attrs.instanceType ? `AWS RDS ${attrs.instanceType} (${attrs.databaseEngine ?? "MySQL"})` : void 0,
|
|
602
719
|
attributes: {
|
|
603
720
|
instance_type: attrs.instanceType ?? "",
|
|
604
721
|
database_engine: attrs.databaseEngine ?? "",
|
|
605
722
|
deployment_option: attrs.deploymentOption ?? "Single-AZ",
|
|
606
723
|
vcpu: attrs.vcpu ?? "",
|
|
607
|
-
memory: attrs.memory ?? ""
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
611
|
-
};
|
|
724
|
+
memory: attrs.memory ?? ""
|
|
725
|
+
}
|
|
726
|
+
});
|
|
612
727
|
}
|
|
613
|
-
function normalizeAwsStorage(rawProduct, rawPrice, region) {
|
|
728
|
+
function normalizeAwsStorage(rawProduct, rawPrice, region, effectiveDate) {
|
|
614
729
|
const attrs = rawProduct?.attributes ?? {};
|
|
615
|
-
const terms = rawPrice?.terms?.OnDemand ?? {};
|
|
616
|
-
const { price, unit } = extractFromTerms(terms, "GB-Mo");
|
|
617
730
|
const volumeType = attrs.volumeApiName ?? attrs.volumeType ?? "gp3";
|
|
618
|
-
return {
|
|
619
|
-
provider: "aws",
|
|
731
|
+
return normalize(rawPrice, region, effectiveDate, {
|
|
620
732
|
service: "ebs",
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
unit,
|
|
624
|
-
price_per_unit: price,
|
|
625
|
-
currency: "USD",
|
|
733
|
+
defaultUnit: "GB-Mo",
|
|
734
|
+
resourceType: volumeType,
|
|
626
735
|
description: `AWS EBS ${volumeType} volume`,
|
|
627
736
|
attributes: {
|
|
628
737
|
volume_type: volumeType,
|
|
629
738
|
max_iops: attrs.maxIopsvolume ?? "",
|
|
630
|
-
max_throughput: attrs.maxThroughputvolume ?? ""
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
634
|
-
};
|
|
739
|
+
max_throughput: attrs.maxThroughputvolume ?? ""
|
|
740
|
+
}
|
|
741
|
+
});
|
|
635
742
|
}
|
|
636
743
|
|
|
637
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
|
+
}
|
|
638
790
|
async function fetchWithRetry(url, init, options = {}) {
|
|
639
791
|
const maxRetries = options.maxRetries ?? 3;
|
|
640
792
|
const baseDelay = options.baseDelay ?? 1e3;
|
|
641
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);
|
|
642
798
|
let lastError;
|
|
643
799
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
644
800
|
try {
|
|
645
|
-
const res = await fetch(url,
|
|
801
|
+
const res = await fetch(url, safeInit);
|
|
646
802
|
if (res.status === 429 || res.status >= 500 && res.status < 600) {
|
|
647
803
|
if (attempt < maxRetries) {
|
|
648
804
|
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
|
|
@@ -656,7 +812,7 @@ async function fetchWithRetry(url, init, options = {}) {
|
|
|
656
812
|
continue;
|
|
657
813
|
}
|
|
658
814
|
}
|
|
659
|
-
return res;
|
|
815
|
+
return capResponseBody(res, maxResponseBytes);
|
|
660
816
|
} catch (err) {
|
|
661
817
|
lastError = err;
|
|
662
818
|
if (attempt < maxRetries) {
|
|
@@ -677,13 +833,14 @@ async function fetchWithRetry(url, init, options = {}) {
|
|
|
677
833
|
}
|
|
678
834
|
}
|
|
679
835
|
}
|
|
680
|
-
|
|
836
|
+
if (lastError instanceof Error) throw lastError;
|
|
837
|
+
throw new Error(`fetchWithRetry: all ${maxRetries} retries failed for ${url}`);
|
|
681
838
|
}
|
|
682
839
|
function sleep(ms) {
|
|
683
840
|
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
|
684
841
|
return Promise.resolve();
|
|
685
842
|
}
|
|
686
|
-
return new Promise((
|
|
843
|
+
return new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
687
844
|
}
|
|
688
845
|
var CIRCUIT_FAILURE_THRESHOLD = 5;
|
|
689
846
|
var CIRCUIT_OPEN_DURATION_MS = 5 * 60 * 1e3;
|
|
@@ -901,6 +1058,11 @@ var EC2_BASE_PRICES = {
|
|
|
901
1058
|
"m7i.large": 0.1008,
|
|
902
1059
|
"m7i.xlarge": 0.2016,
|
|
903
1060
|
"m7i.2xlarge": 0.4032,
|
|
1061
|
+
"m7i-flex.large": 0.0958,
|
|
1062
|
+
"m7i-flex.xlarge": 0.1915,
|
|
1063
|
+
"m7i-flex.2xlarge": 0.383,
|
|
1064
|
+
"m7i-flex.4xlarge": 0.7661,
|
|
1065
|
+
"m7i-flex.8xlarge": 1.5322,
|
|
904
1066
|
"m7g.large": 0.0816,
|
|
905
1067
|
"m7g.xlarge": 0.1632,
|
|
906
1068
|
"m7g.2xlarge": 0.3264,
|
|
@@ -918,10 +1080,10 @@ var EC2_BASE_PRICES = {
|
|
|
918
1080
|
"c6g.xlarge": 0.136,
|
|
919
1081
|
"c6g.2xlarge": 0.272,
|
|
920
1082
|
"c6g.4xlarge": 0.544,
|
|
921
|
-
"c7i.large": 0.
|
|
922
|
-
"c7i.xlarge": 0.
|
|
1083
|
+
"c7i.large": 0.0893,
|
|
1084
|
+
"c7i.xlarge": 0.1785,
|
|
923
1085
|
"c7i.2xlarge": 0.357,
|
|
924
|
-
"c7g.large": 0.
|
|
1086
|
+
"c7g.large": 0.0725,
|
|
925
1087
|
"c7g.xlarge": 0.145,
|
|
926
1088
|
"c7g.2xlarge": 0.29,
|
|
927
1089
|
"c7g.4xlarge": 0.58,
|
|
@@ -934,65 +1096,63 @@ var EC2_BASE_PRICES = {
|
|
|
934
1096
|
"r6i.large": 0.126,
|
|
935
1097
|
"r6i.xlarge": 0.252,
|
|
936
1098
|
"r6i.2xlarge": 0.504,
|
|
937
|
-
"r6g.large": 0.
|
|
938
|
-
"r6g.xlarge": 0.
|
|
939
|
-
"r6g.2xlarge": 0.
|
|
940
|
-
"r6g.4xlarge": 0.
|
|
941
|
-
"r7i.large": 0.
|
|
942
|
-
"r7i.xlarge": 0.
|
|
943
|
-
"r7i.2xlarge": 0.
|
|
944
|
-
"r7g.large": 0.
|
|
945
|
-
"r7g.xlarge": 0.
|
|
946
|
-
"r7g.2xlarge": 0.
|
|
947
|
-
"r7g.4xlarge": 0.
|
|
948
|
-
|
|
949
|
-
"m8g.
|
|
950
|
-
"m8g.
|
|
951
|
-
"m8g.
|
|
952
|
-
"
|
|
953
|
-
"c8g.
|
|
954
|
-
"c8g.
|
|
955
|
-
"c8g.
|
|
956
|
-
"
|
|
957
|
-
"r8g.
|
|
958
|
-
"r8g.
|
|
959
|
-
"r8g.
|
|
960
|
-
"r8g.4xlarge": 0.808,
|
|
961
|
-
// GPU instances
|
|
1099
|
+
"r6g.large": 0.1008,
|
|
1100
|
+
"r6g.xlarge": 0.2016,
|
|
1101
|
+
"r6g.2xlarge": 0.4032,
|
|
1102
|
+
"r6g.4xlarge": 0.8064,
|
|
1103
|
+
"r7i.large": 0.1323,
|
|
1104
|
+
"r7i.xlarge": 0.2646,
|
|
1105
|
+
"r7i.2xlarge": 0.5292,
|
|
1106
|
+
"r7g.large": 0.1071,
|
|
1107
|
+
"r7g.xlarge": 0.2142,
|
|
1108
|
+
"r7g.2xlarge": 0.4284,
|
|
1109
|
+
"r7g.4xlarge": 0.8568,
|
|
1110
|
+
"m8g.large": 0.0898,
|
|
1111
|
+
"m8g.xlarge": 0.1795,
|
|
1112
|
+
"m8g.2xlarge": 0.359,
|
|
1113
|
+
"m8g.4xlarge": 0.7181,
|
|
1114
|
+
"c8g.large": 0.0798,
|
|
1115
|
+
"c8g.xlarge": 0.1595,
|
|
1116
|
+
"c8g.2xlarge": 0.319,
|
|
1117
|
+
"c8g.4xlarge": 0.6381,
|
|
1118
|
+
"r8g.large": 0.1178,
|
|
1119
|
+
"r8g.xlarge": 0.2356,
|
|
1120
|
+
"r8g.2xlarge": 0.4713,
|
|
1121
|
+
"r8g.4xlarge": 0.9426,
|
|
962
1122
|
"g5.xlarge": 1.006,
|
|
963
1123
|
"g5.2xlarge": 1.212,
|
|
964
1124
|
"g5.4xlarge": 1.624,
|
|
965
1125
|
"g5.8xlarge": 2.448,
|
|
966
1126
|
"g5.12xlarge": 5.672,
|
|
967
|
-
"p4d.24xlarge":
|
|
1127
|
+
"p4d.24xlarge": 21.9576
|
|
968
1128
|
};
|
|
969
1129
|
var RDS_BASE_PRICES = {
|
|
970
|
-
"db.t3.micro": 0.
|
|
971
|
-
"db.t3.small": 0.
|
|
972
|
-
"db.t3.medium": 0.
|
|
973
|
-
"db.t3.large": 0.
|
|
1130
|
+
"db.t3.micro": 0.018,
|
|
1131
|
+
"db.t3.small": 0.036,
|
|
1132
|
+
"db.t3.medium": 0.072,
|
|
1133
|
+
"db.t3.large": 0.145,
|
|
974
1134
|
"db.t4g.micro": 0.016,
|
|
975
1135
|
"db.t4g.small": 0.032,
|
|
976
1136
|
"db.t4g.medium": 0.065,
|
|
977
1137
|
"db.t4g.large": 0.129,
|
|
978
1138
|
"db.t4g.xlarge": 0.258,
|
|
979
|
-
"db.m5.large": 0.
|
|
980
|
-
"db.m5.xlarge": 0.
|
|
981
|
-
"db.m6g.large": 0.
|
|
982
|
-
"db.m6g.xlarge": 0.
|
|
983
|
-
"db.m6g.2xlarge": 0.
|
|
984
|
-
"db.m6i.large": 0.
|
|
985
|
-
"db.m6i.xlarge": 0.
|
|
986
|
-
"db.m6i.2xlarge": 0.
|
|
1139
|
+
"db.m5.large": 0.178,
|
|
1140
|
+
"db.m5.xlarge": 0.356,
|
|
1141
|
+
"db.m6g.large": 0.159,
|
|
1142
|
+
"db.m6g.xlarge": 0.318,
|
|
1143
|
+
"db.m6g.2xlarge": 0.636,
|
|
1144
|
+
"db.m6i.large": 0.178,
|
|
1145
|
+
"db.m6i.xlarge": 0.356,
|
|
1146
|
+
"db.m6i.2xlarge": 0.712,
|
|
987
1147
|
"db.m7g.large": 0.168,
|
|
988
|
-
"db.m7g.xlarge": 0.
|
|
989
|
-
"db.m7g.2xlarge": 0.
|
|
1148
|
+
"db.m7g.xlarge": 0.337,
|
|
1149
|
+
"db.m7g.2xlarge": 0.674,
|
|
990
1150
|
"db.r5.large": 0.25,
|
|
991
1151
|
"db.r5.xlarge": 0.5,
|
|
992
1152
|
"db.r5.2xlarge": 1,
|
|
993
|
-
"db.r6g.large": 0.
|
|
994
|
-
"db.r6g.xlarge": 0.
|
|
995
|
-
"db.r6g.2xlarge": 0.
|
|
1153
|
+
"db.r6g.large": 0.225,
|
|
1154
|
+
"db.r6g.xlarge": 0.45,
|
|
1155
|
+
"db.r6g.2xlarge": 0.899,
|
|
996
1156
|
"db.r6i.large": 0.25,
|
|
997
1157
|
"db.r6i.xlarge": 0.5,
|
|
998
1158
|
"db.r6i.2xlarge": 1
|
|
@@ -1012,6 +1172,24 @@ var NAT_PER_GB = 0.045;
|
|
|
1012
1172
|
var EKS_HOURLY = 0.1;
|
|
1013
1173
|
var CACHE_TTL = 86400;
|
|
1014
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
|
+
}
|
|
1015
1193
|
var SIZE_ORDER = [
|
|
1016
1194
|
"nano",
|
|
1017
1195
|
"micro",
|
|
@@ -1028,11 +1206,12 @@ var SIZE_ORDER = [
|
|
|
1028
1206
|
"32xlarge",
|
|
1029
1207
|
"48xlarge"
|
|
1030
1208
|
];
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1209
|
+
|
|
1210
|
+
// src/pricing/region-multiplier.ts
|
|
1211
|
+
function regionMultiplier(provider, region) {
|
|
1212
|
+
const mult = getRegionPriceMultipliers()[provider][region.toLowerCase()];
|
|
1034
1213
|
if (mult === void 0) {
|
|
1035
|
-
logger.warn("region-multiplier: unknown
|
|
1214
|
+
logger.warn("region-multiplier: unknown region, defaulting to 1.0x", { provider, region });
|
|
1036
1215
|
return 1;
|
|
1037
1216
|
}
|
|
1038
1217
|
return mult;
|
|
@@ -1041,13 +1220,16 @@ function regionMultiplier(region) {
|
|
|
1041
1220
|
// src/pricing/aws/bulk-loader.ts
|
|
1042
1221
|
var AwsBulkLoader = class {
|
|
1043
1222
|
cache;
|
|
1223
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
1224
|
+
ttlSeconds;
|
|
1044
1225
|
/**
|
|
1045
1226
|
* Deduplicates concurrent streaming CSV fetches for the same region.
|
|
1046
1227
|
* Keyed by region; value is the in-flight Promise<boolean>.
|
|
1047
1228
|
*/
|
|
1048
1229
|
ec2CsvFetchInProgress = /* @__PURE__ */ new Map();
|
|
1049
|
-
constructor(cache) {
|
|
1230
|
+
constructor(cache, ttlSeconds = CACHE_TTL) {
|
|
1050
1231
|
this.cache = cache;
|
|
1232
|
+
this.ttlSeconds = ttlSeconds;
|
|
1051
1233
|
}
|
|
1052
1234
|
// -------------------------------------------------------------------------
|
|
1053
1235
|
// Public API
|
|
@@ -1074,7 +1256,7 @@ var AwsBulkLoader = class {
|
|
|
1074
1256
|
if (bulk) {
|
|
1075
1257
|
const result = this.extractEc2Price(bulk, instanceType, region, os);
|
|
1076
1258
|
if (result) {
|
|
1077
|
-
this.cache.set(cacheKey, result, "aws", "ec2", region,
|
|
1259
|
+
this.cache.set(cacheKey, result, "aws", "ec2", region, this.ttlSeconds);
|
|
1078
1260
|
return result;
|
|
1079
1261
|
}
|
|
1080
1262
|
}
|
|
@@ -1096,7 +1278,7 @@ var AwsBulkLoader = class {
|
|
|
1096
1278
|
if (bulk) {
|
|
1097
1279
|
const result = this.extractRdsPrice(bulk, instanceClass, region, engine);
|
|
1098
1280
|
if (result) {
|
|
1099
|
-
this.cache.set(cacheKey, result, "aws", "rds", region,
|
|
1281
|
+
this.cache.set(cacheKey, result, "aws", "rds", region, this.ttlSeconds);
|
|
1100
1282
|
return result;
|
|
1101
1283
|
}
|
|
1102
1284
|
}
|
|
@@ -1118,7 +1300,7 @@ var AwsBulkLoader = class {
|
|
|
1118
1300
|
if (bulk) {
|
|
1119
1301
|
const result = this.extractEbsPrice(bulk, volumeType, region);
|
|
1120
1302
|
if (result) {
|
|
1121
|
-
this.cache.set(cacheKey, result, "aws", "ebs", region,
|
|
1303
|
+
this.cache.set(cacheKey, result, "aws", "ebs", region, this.ttlSeconds);
|
|
1122
1304
|
return result;
|
|
1123
1305
|
}
|
|
1124
1306
|
}
|
|
@@ -1131,9 +1313,9 @@ var AwsBulkLoader = class {
|
|
|
1131
1313
|
}
|
|
1132
1314
|
return this.fallbackStoragePrice(volumeType, region, cacheKey);
|
|
1133
1315
|
}
|
|
1134
|
-
|
|
1135
|
-
const multiplier = regionMultiplier(region);
|
|
1136
|
-
return {
|
|
1316
|
+
getLoadBalancerPrice(region) {
|
|
1317
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1318
|
+
return Promise.resolve({
|
|
1137
1319
|
provider: "aws",
|
|
1138
1320
|
service: "elb",
|
|
1139
1321
|
resource_type: "alb",
|
|
@@ -1147,11 +1329,11 @@ var AwsBulkLoader = class {
|
|
|
1147
1329
|
pricing_model: "fixed_plus_lcu"
|
|
1148
1330
|
},
|
|
1149
1331
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1150
|
-
};
|
|
1332
|
+
});
|
|
1151
1333
|
}
|
|
1152
|
-
|
|
1153
|
-
const multiplier = regionMultiplier(region);
|
|
1154
|
-
return {
|
|
1334
|
+
getNatGatewayPrice(region) {
|
|
1335
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1336
|
+
return Promise.resolve({
|
|
1155
1337
|
provider: "aws",
|
|
1156
1338
|
service: "vpc",
|
|
1157
1339
|
resource_type: "nat-gateway",
|
|
@@ -1164,11 +1346,11 @@ var AwsBulkLoader = class {
|
|
|
1164
1346
|
per_gb_price: String(NAT_PER_GB * multiplier)
|
|
1165
1347
|
},
|
|
1166
1348
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1167
|
-
};
|
|
1349
|
+
});
|
|
1168
1350
|
}
|
|
1169
|
-
|
|
1170
|
-
const multiplier = regionMultiplier(region);
|
|
1171
|
-
return {
|
|
1351
|
+
getKubernetesPrice(region) {
|
|
1352
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1353
|
+
return Promise.resolve({
|
|
1172
1354
|
provider: "aws",
|
|
1173
1355
|
service: "eks",
|
|
1174
1356
|
resource_type: "cluster",
|
|
@@ -1179,7 +1361,7 @@ var AwsBulkLoader = class {
|
|
|
1179
1361
|
description: "AWS EKS control plane (per cluster/hour)",
|
|
1180
1362
|
attributes: {},
|
|
1181
1363
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1182
|
-
};
|
|
1364
|
+
});
|
|
1183
1365
|
}
|
|
1184
1366
|
// -------------------------------------------------------------------------
|
|
1185
1367
|
// Internal helpers – EC2 CSV streaming
|
|
@@ -1209,7 +1391,8 @@ var AwsBulkLoader = class {
|
|
|
1209
1391
|
return promise;
|
|
1210
1392
|
}
|
|
1211
1393
|
async _doFetchAndCacheEc2CsvPrices(region) {
|
|
1212
|
-
|
|
1394
|
+
assertValidAwsRegion(region);
|
|
1395
|
+
const url = `${BULK_PRICING_BASE}/AmazonEC2/current/${encodeURIComponent(region)}/index.csv`;
|
|
1213
1396
|
logger.debug("Streaming AWS EC2 CSV pricing", { url, region });
|
|
1214
1397
|
const controller = new AbortController();
|
|
1215
1398
|
const timeoutId = setTimeout(() => controller.abort(), 12e4);
|
|
@@ -1220,7 +1403,11 @@ var AwsBulkLoader = class {
|
|
|
1220
1403
|
{
|
|
1221
1404
|
maxRetries: 1,
|
|
1222
1405
|
baseDelay: 500,
|
|
1223
|
-
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
|
|
1224
1411
|
}
|
|
1225
1412
|
);
|
|
1226
1413
|
if (!res.ok) {
|
|
@@ -1247,7 +1434,7 @@ var AwsBulkLoader = class {
|
|
|
1247
1434
|
let headerFound = false;
|
|
1248
1435
|
let leftover = "";
|
|
1249
1436
|
let cachedCount = 0;
|
|
1250
|
-
|
|
1437
|
+
let effectiveDate = (/* @__PURE__ */ new Date()).toISOString();
|
|
1251
1438
|
while (true) {
|
|
1252
1439
|
const { value, done } = await reader.read();
|
|
1253
1440
|
if (done) break;
|
|
@@ -1258,6 +1445,17 @@ var AwsBulkLoader = class {
|
|
|
1258
1445
|
const line = rawLine.trimEnd();
|
|
1259
1446
|
if (!line) continue;
|
|
1260
1447
|
if (!headerFound) {
|
|
1448
|
+
if (line.startsWith('"Publication Date"') || line.startsWith("Publication Date")) {
|
|
1449
|
+
const metaFields = parseCsvLine(line);
|
|
1450
|
+
const rawDate = (metaFields[1] ?? "").trim();
|
|
1451
|
+
if (rawDate) {
|
|
1452
|
+
const parsed = new Date(rawDate);
|
|
1453
|
+
if (!isNaN(parsed.getTime())) {
|
|
1454
|
+
effectiveDate = parsed.toISOString();
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1261
1459
|
if (line.startsWith('"SKU"') || line.startsWith("SKU")) {
|
|
1262
1460
|
const headers = parseCsvLine(line);
|
|
1263
1461
|
for (let h = 0; h < headers.length; h++) {
|
|
@@ -1330,7 +1528,7 @@ var AwsBulkLoader = class {
|
|
|
1330
1528
|
},
|
|
1331
1529
|
effective_date: effectiveDate
|
|
1332
1530
|
};
|
|
1333
|
-
this.cache.set(csvCacheKey, normalized, "aws", "ec2", region,
|
|
1531
|
+
this.cache.set(csvCacheKey, normalized, "aws", "ec2", region, this.ttlSeconds);
|
|
1334
1532
|
cachedCount++;
|
|
1335
1533
|
}
|
|
1336
1534
|
}
|
|
@@ -1362,12 +1560,12 @@ var AwsBulkLoader = class {
|
|
|
1362
1560
|
tenancy: "Shared",
|
|
1363
1561
|
pricing_source: "live"
|
|
1364
1562
|
},
|
|
1365
|
-
effective_date:
|
|
1563
|
+
effective_date: effectiveDate
|
|
1366
1564
|
},
|
|
1367
1565
|
"aws",
|
|
1368
1566
|
"ec2",
|
|
1369
1567
|
region,
|
|
1370
|
-
|
|
1568
|
+
this.ttlSeconds
|
|
1371
1569
|
);
|
|
1372
1570
|
cachedCount++;
|
|
1373
1571
|
}
|
|
@@ -1399,14 +1597,19 @@ var AwsBulkLoader = class {
|
|
|
1399
1597
|
// Internal helpers – live API fetch
|
|
1400
1598
|
// -------------------------------------------------------------------------
|
|
1401
1599
|
async fetchBulkPricing(service, region) {
|
|
1402
|
-
|
|
1600
|
+
assertValidAwsPricingService(service);
|
|
1601
|
+
assertValidAwsRegion(region);
|
|
1602
|
+
const url = `${BULK_PRICING_BASE}/${encodeURIComponent(service)}/current/${encodeURIComponent(region)}/index.json`;
|
|
1403
1603
|
logger.debug("Fetching AWS bulk pricing", { url });
|
|
1404
1604
|
const res = await fetchWithRetryAndCircuitBreaker(
|
|
1405
1605
|
url,
|
|
1406
1606
|
{
|
|
1407
1607
|
signal: AbortSignal.timeout(3e4)
|
|
1408
1608
|
},
|
|
1409
|
-
{
|
|
1609
|
+
{
|
|
1610
|
+
maxRetries: 0,
|
|
1611
|
+
allowedHosts: [AWS_PRICING_HOST]
|
|
1612
|
+
}
|
|
1410
1613
|
);
|
|
1411
1614
|
if (!res.ok) {
|
|
1412
1615
|
throw new Error(`HTTP ${res.status} from ${url}`);
|
|
@@ -1422,6 +1625,7 @@ var AwsBulkLoader = class {
|
|
|
1422
1625
|
extractEc2Price(bulk, instanceType, region, os) {
|
|
1423
1626
|
const products = bulk?.products ?? {};
|
|
1424
1627
|
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
1628
|
+
const publicationDate = bulk?.publicationDate;
|
|
1425
1629
|
const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
|
|
1426
1630
|
for (const [sku, product] of sortedEntries) {
|
|
1427
1631
|
const attrs = product?.attributes ?? {};
|
|
@@ -1431,7 +1635,8 @@ var AwsBulkLoader = class {
|
|
|
1431
1635
|
return normalizeAwsCompute(
|
|
1432
1636
|
product,
|
|
1433
1637
|
{ terms: { OnDemand: { [sku]: priceTerms } } },
|
|
1434
|
-
region
|
|
1638
|
+
region,
|
|
1639
|
+
publicationDate
|
|
1435
1640
|
);
|
|
1436
1641
|
}
|
|
1437
1642
|
}
|
|
@@ -1441,6 +1646,7 @@ var AwsBulkLoader = class {
|
|
|
1441
1646
|
extractRdsPrice(bulk, instanceClass, region, engine) {
|
|
1442
1647
|
const products = bulk?.products ?? {};
|
|
1443
1648
|
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
1649
|
+
const publicationDate = bulk?.publicationDate;
|
|
1444
1650
|
const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
|
|
1445
1651
|
for (const [sku, product] of sortedEntries) {
|
|
1446
1652
|
const attrs = product?.attributes ?? {};
|
|
@@ -1450,7 +1656,8 @@ var AwsBulkLoader = class {
|
|
|
1450
1656
|
return normalizeAwsDatabase(
|
|
1451
1657
|
product,
|
|
1452
1658
|
{ terms: { OnDemand: { [sku]: priceTerms } } },
|
|
1453
|
-
region
|
|
1659
|
+
region,
|
|
1660
|
+
publicationDate
|
|
1454
1661
|
);
|
|
1455
1662
|
}
|
|
1456
1663
|
}
|
|
@@ -1460,6 +1667,7 @@ var AwsBulkLoader = class {
|
|
|
1460
1667
|
extractEbsPrice(bulk, volumeType, region) {
|
|
1461
1668
|
const products = bulk?.products ?? {};
|
|
1462
1669
|
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
1670
|
+
const publicationDate = bulk?.publicationDate;
|
|
1463
1671
|
const sortedEntries = Object.entries(products).sort(([a], [b]) => a.localeCompare(b));
|
|
1464
1672
|
for (const [sku, product] of sortedEntries) {
|
|
1465
1673
|
const attrs = product?.attributes ?? {};
|
|
@@ -1470,7 +1678,8 @@ var AwsBulkLoader = class {
|
|
|
1470
1678
|
return normalizeAwsStorage(
|
|
1471
1679
|
product,
|
|
1472
1680
|
{ terms: { OnDemand: { [sku]: priceTerms } } },
|
|
1473
|
-
region
|
|
1681
|
+
region,
|
|
1682
|
+
publicationDate
|
|
1474
1683
|
);
|
|
1475
1684
|
}
|
|
1476
1685
|
}
|
|
@@ -1486,7 +1695,7 @@ var AwsBulkLoader = class {
|
|
|
1486
1695
|
logger.warn("No fallback price found for EC2 instance type", { instanceType });
|
|
1487
1696
|
return null;
|
|
1488
1697
|
}
|
|
1489
|
-
const multiplier = regionMultiplier(region);
|
|
1698
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1490
1699
|
const result = {
|
|
1491
1700
|
provider: "aws",
|
|
1492
1701
|
service: "ec2",
|
|
@@ -1504,7 +1713,7 @@ var AwsBulkLoader = class {
|
|
|
1504
1713
|
},
|
|
1505
1714
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1506
1715
|
};
|
|
1507
|
-
this.cache.set(cacheKey, result, "aws", "ec2", region,
|
|
1716
|
+
this.cache.set(cacheKey, result, "aws", "ec2", region, this.ttlSeconds);
|
|
1508
1717
|
return result;
|
|
1509
1718
|
}
|
|
1510
1719
|
fallbackDatabasePrice(instanceClass, region, engine, cacheKey) {
|
|
@@ -1513,7 +1722,7 @@ var AwsBulkLoader = class {
|
|
|
1513
1722
|
logger.warn("No fallback price found for RDS instance class", { instanceClass });
|
|
1514
1723
|
return null;
|
|
1515
1724
|
}
|
|
1516
|
-
const multiplier = regionMultiplier(region);
|
|
1725
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1517
1726
|
const result = {
|
|
1518
1727
|
provider: "aws",
|
|
1519
1728
|
service: "rds",
|
|
@@ -1531,7 +1740,7 @@ var AwsBulkLoader = class {
|
|
|
1531
1740
|
},
|
|
1532
1741
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1533
1742
|
};
|
|
1534
|
-
this.cache.set(cacheKey, result, "aws", "rds", region,
|
|
1743
|
+
this.cache.set(cacheKey, result, "aws", "rds", region, this.ttlSeconds);
|
|
1535
1744
|
return result;
|
|
1536
1745
|
}
|
|
1537
1746
|
fallbackStoragePrice(volumeType, region, cacheKey) {
|
|
@@ -1540,7 +1749,7 @@ var AwsBulkLoader = class {
|
|
|
1540
1749
|
logger.warn("No fallback price found for EBS volume type", { volumeType });
|
|
1541
1750
|
return null;
|
|
1542
1751
|
}
|
|
1543
|
-
const multiplier = regionMultiplier(region);
|
|
1752
|
+
const multiplier = regionMultiplier("aws", region);
|
|
1544
1753
|
const result = {
|
|
1545
1754
|
provider: "aws",
|
|
1546
1755
|
service: "ebs",
|
|
@@ -1556,74 +1765,411 @@ var AwsBulkLoader = class {
|
|
|
1556
1765
|
},
|
|
1557
1766
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1558
1767
|
};
|
|
1559
|
-
this.cache.set(cacheKey, result, "aws", "ebs", region,
|
|
1768
|
+
this.cache.set(cacheKey, result, "aws", "ebs", region, this.ttlSeconds);
|
|
1560
1769
|
return result;
|
|
1561
1770
|
}
|
|
1562
1771
|
};
|
|
1563
1772
|
|
|
1564
|
-
// src/pricing/
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1773
|
+
// src/pricing/aws/spot-client.ts
|
|
1774
|
+
var SPOT_ADVISOR_URL = "https://website.spot.ec2.aws.a2z.com/spot.json";
|
|
1775
|
+
var SPOT_ADVISOR_CACHE_KEY = "aws/spot-advisor/v1";
|
|
1776
|
+
var AwsSpotClient = class {
|
|
1777
|
+
cache;
|
|
1778
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
1779
|
+
ttlSeconds;
|
|
1780
|
+
/** In-flight fetch deduplication. */
|
|
1781
|
+
inflight = null;
|
|
1782
|
+
constructor(cache, ttlSeconds = CACHE_TTL) {
|
|
1783
|
+
this.cache = cache;
|
|
1784
|
+
this.ttlSeconds = ttlSeconds;
|
|
1785
|
+
}
|
|
1786
|
+
/**
|
|
1787
|
+
* Get the spot discount factor (0–1) for an instance type in a region.
|
|
1788
|
+
*
|
|
1789
|
+
* Returns a fraction representing the **portion of on-demand you pay** — i.e.
|
|
1790
|
+
* `hourly_spot = hourly_on_demand * factor`. Returns null when the live data
|
|
1791
|
+
* is unavailable or doesn't cover the given instance/region combination;
|
|
1792
|
+
* callers should fall back to static family-based estimates in that case.
|
|
1793
|
+
*/
|
|
1794
|
+
async getSpotFactor(instanceType, region, os = "Linux") {
|
|
1795
|
+
try {
|
|
1796
|
+
const doc = await this.loadDocument();
|
|
1797
|
+
if (!doc) return null;
|
|
1798
|
+
const regionData = doc.spot_advisor?.[region];
|
|
1799
|
+
if (!regionData) return null;
|
|
1800
|
+
const osKey = os === "Windows" ? "Windows" : "Linux";
|
|
1801
|
+
const osData = regionData[osKey];
|
|
1802
|
+
if (!osData) return null;
|
|
1803
|
+
const entry = osData[instanceType];
|
|
1804
|
+
if (!entry || typeof entry.s !== "number") return null;
|
|
1805
|
+
const savingsPct = Math.max(0, Math.min(95, entry.s));
|
|
1806
|
+
const factor = 1 - savingsPct / 100;
|
|
1807
|
+
return Math.max(0.05, factor);
|
|
1808
|
+
} catch (err) {
|
|
1809
|
+
logger.debug("AwsSpotClient.getSpotFactor failed", {
|
|
1810
|
+
instanceType,
|
|
1811
|
+
region,
|
|
1812
|
+
err: err instanceof Error ? err.message : String(err)
|
|
1813
|
+
});
|
|
1814
|
+
return null;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Fetch and cache the full Spot Advisor document. Uses the shared SQLite
|
|
1819
|
+
* cache with the same TTL as other AWS bulk data. Deduplicates concurrent
|
|
1820
|
+
* callers via an in-flight promise.
|
|
1821
|
+
*/
|
|
1822
|
+
loadDocument() {
|
|
1823
|
+
const cached = this.cache.get(SPOT_ADVISOR_CACHE_KEY);
|
|
1824
|
+
if (cached) return Promise.resolve(cached);
|
|
1825
|
+
if (this.inflight) return this.inflight;
|
|
1826
|
+
this.inflight = this.doFetchDocument().finally(() => {
|
|
1827
|
+
this.inflight = null;
|
|
1828
|
+
});
|
|
1829
|
+
return this.inflight;
|
|
1830
|
+
}
|
|
1831
|
+
async doFetchDocument() {
|
|
1832
|
+
logger.debug("Fetching AWS Spot Advisor JSON", { url: SPOT_ADVISOR_URL });
|
|
1833
|
+
try {
|
|
1834
|
+
const res = await fetchWithRetryAndCircuitBreaker(
|
|
1835
|
+
SPOT_ADVISOR_URL,
|
|
1836
|
+
{ signal: AbortSignal.timeout(2e4) },
|
|
1837
|
+
{ maxRetries: 1, baseDelay: 500, maxDelay: 2e3 }
|
|
1838
|
+
);
|
|
1839
|
+
if (!res.ok) {
|
|
1840
|
+
logger.debug("AWS Spot Advisor fetch returned non-OK", { status: res.status });
|
|
1841
|
+
return null;
|
|
1842
|
+
}
|
|
1843
|
+
const doc = await res.json();
|
|
1844
|
+
if (!doc || typeof doc.spot_advisor !== "object") {
|
|
1845
|
+
logger.debug("AWS Spot Advisor response missing spot_advisor key");
|
|
1846
|
+
return null;
|
|
1847
|
+
}
|
|
1848
|
+
this.cache.set(SPOT_ADVISOR_CACHE_KEY, doc, "aws", "spot-advisor", "global", this.ttlSeconds);
|
|
1849
|
+
return doc;
|
|
1850
|
+
} catch (err) {
|
|
1851
|
+
logger.debug("AWS Spot Advisor fetch failed", {
|
|
1852
|
+
err: err instanceof Error ? err.message : String(err)
|
|
1853
|
+
});
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
// src/pricing/aws/reserved-client.ts
|
|
1860
|
+
var HOURS_IN_TERM = {
|
|
1861
|
+
"1yr": 24 * 365,
|
|
1862
|
+
"3yr": 24 * 365 * 3
|
|
1863
|
+
};
|
|
1864
|
+
function extractRiRatesFromBulk(bulk, instanceType, os = "Linux") {
|
|
1865
|
+
const products = bulk?.products ?? {};
|
|
1866
|
+
const onDemand = bulk?.terms?.OnDemand ?? {};
|
|
1867
|
+
const reserved = bulk?.terms?.Reserved ?? {};
|
|
1868
|
+
let targetSku = null;
|
|
1869
|
+
for (const [sku, product] of Object.entries(products)) {
|
|
1870
|
+
const attrs = product?.attributes ?? {};
|
|
1871
|
+
if (attrs.instanceType === instanceType && (attrs.operatingSystem ?? "").toLowerCase() === os.toLowerCase() && attrs.tenancy === "Shared" && attrs.capacitystatus === "Used") {
|
|
1872
|
+
targetSku = sku;
|
|
1873
|
+
break;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (!targetSku) return [];
|
|
1877
|
+
const onDemandHourly = firstHourlyPrice(onDemand[targetSku]);
|
|
1878
|
+
if (!onDemandHourly || onDemandHourly <= 0) return [];
|
|
1879
|
+
const reservedOffers = reserved[targetSku];
|
|
1880
|
+
if (!reservedOffers) return [];
|
|
1881
|
+
const rates = [];
|
|
1882
|
+
for (const offer of Object.values(reservedOffers)) {
|
|
1883
|
+
const lease = offer.termAttributes?.LeaseContractLength;
|
|
1884
|
+
const purchase = offer.termAttributes?.PurchaseOption;
|
|
1885
|
+
const term = normalizeTerm(lease);
|
|
1886
|
+
const payment = normalizePayment(purchase);
|
|
1887
|
+
if (!term || !payment) continue;
|
|
1888
|
+
const effectiveHourly = computeEffectiveHourly(offer, term);
|
|
1889
|
+
if (effectiveHourly === null) continue;
|
|
1890
|
+
const rate = 1 - effectiveHourly / onDemandHourly;
|
|
1891
|
+
if (rate <= 0 || rate >= 0.9) continue;
|
|
1892
|
+
rates.push({ term, payment, rate });
|
|
1893
|
+
}
|
|
1894
|
+
return rates;
|
|
1895
|
+
}
|
|
1896
|
+
var AwsReservedClient = class {
|
|
1897
|
+
cache;
|
|
1898
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
1899
|
+
ttlSeconds;
|
|
1900
|
+
constructor(cache, ttlSeconds = CACHE_TTL) {
|
|
1901
|
+
this.cache = cache;
|
|
1902
|
+
this.ttlSeconds = ttlSeconds;
|
|
1903
|
+
}
|
|
1904
|
+
async getRiRates(service, instanceType, region, os = "Linux") {
|
|
1905
|
+
if (service === "AmazonEC2") {
|
|
1906
|
+
logger.debug(
|
|
1907
|
+
"AwsReservedClient: refusing AmazonEC2 (bulk JSON too large for in-memory parse)",
|
|
1908
|
+
{
|
|
1909
|
+
instanceType,
|
|
1910
|
+
region
|
|
1911
|
+
}
|
|
1912
|
+
);
|
|
1913
|
+
return null;
|
|
1914
|
+
}
|
|
1915
|
+
const cacheKey = `aws/ri/${service.toLowerCase()}/${region.toLowerCase()}/${instanceType.toLowerCase()}/${os.toLowerCase()}`;
|
|
1916
|
+
const cached = this.cache.get(cacheKey);
|
|
1917
|
+
if (cached && Array.isArray(cached.rates)) return cached.rates;
|
|
1918
|
+
assertValidAwsPricingService(service);
|
|
1919
|
+
assertValidAwsRegion(region);
|
|
1920
|
+
const url = `${BULK_PRICING_BASE}/${encodeURIComponent(service)}/current/${encodeURIComponent(region)}/index.json`;
|
|
1921
|
+
logger.debug("Fetching AWS bulk pricing for RI rates", { url });
|
|
1922
|
+
try {
|
|
1923
|
+
const res = await fetchWithRetryAndCircuitBreaker(
|
|
1924
|
+
url,
|
|
1925
|
+
{ signal: AbortSignal.timeout(6e4) },
|
|
1926
|
+
{
|
|
1927
|
+
maxRetries: 0,
|
|
1928
|
+
allowedHosts: [AWS_PRICING_HOST]
|
|
1929
|
+
}
|
|
1930
|
+
);
|
|
1931
|
+
if (!res.ok) {
|
|
1932
|
+
logger.debug("AWS RI bulk fetch non-OK", { status: res.status });
|
|
1933
|
+
return null;
|
|
1934
|
+
}
|
|
1935
|
+
const bulk = await res.json();
|
|
1936
|
+
const rates = extractRiRatesFromBulk(bulk, instanceType, os);
|
|
1937
|
+
if (rates.length === 0) return null;
|
|
1938
|
+
const entry = {
|
|
1939
|
+
price_per_unit: 0,
|
|
1940
|
+
unit: "none",
|
|
1941
|
+
rates
|
|
1942
|
+
};
|
|
1943
|
+
this.cache.set(cacheKey, entry, "aws", "ri", region, this.ttlSeconds);
|
|
1944
|
+
return rates;
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
logger.debug("AWS RI bulk fetch failed, will use static fallback", {
|
|
1947
|
+
instanceType,
|
|
1948
|
+
region,
|
|
1949
|
+
err: err instanceof Error ? err.message : String(err)
|
|
1950
|
+
});
|
|
1951
|
+
return null;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
};
|
|
1955
|
+
function firstHourlyPrice(terms) {
|
|
1956
|
+
if (!terms) return null;
|
|
1957
|
+
for (const term of Object.values(terms)) {
|
|
1958
|
+
for (const dim of Object.values(term.priceDimensions ?? {})) {
|
|
1959
|
+
if ((dim.unit ?? "").toLowerCase().startsWith("hr")) {
|
|
1960
|
+
const price = parseFloat(dim.pricePerUnit?.USD ?? "");
|
|
1961
|
+
if (isFinite(price) && price > 0) return price;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
return null;
|
|
1966
|
+
}
|
|
1967
|
+
function computeEffectiveHourly(offer, term) {
|
|
1968
|
+
let upfront = 0;
|
|
1969
|
+
let hourly = 0;
|
|
1970
|
+
let sawAnything = false;
|
|
1971
|
+
for (const dim of Object.values(offer.priceDimensions ?? {})) {
|
|
1972
|
+
const unit = (dim.unit ?? "").toLowerCase();
|
|
1973
|
+
const value = parseFloat(dim.pricePerUnit?.USD ?? "");
|
|
1974
|
+
if (!isFinite(value)) continue;
|
|
1975
|
+
if (unit.startsWith("hr")) {
|
|
1976
|
+
hourly = value;
|
|
1977
|
+
sawAnything = true;
|
|
1978
|
+
} else if (unit === "quantity") {
|
|
1979
|
+
upfront = value;
|
|
1980
|
+
sawAnything = true;
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
if (!sawAnything) return null;
|
|
1984
|
+
return hourly + upfront / HOURS_IN_TERM[term];
|
|
1985
|
+
}
|
|
1986
|
+
function normalizeTerm(lease) {
|
|
1987
|
+
if (lease === "1yr") return "1yr";
|
|
1988
|
+
if (lease === "3yr") return "3yr";
|
|
1989
|
+
return null;
|
|
1990
|
+
}
|
|
1991
|
+
function normalizePayment(purchase) {
|
|
1992
|
+
switch (purchase) {
|
|
1993
|
+
case "No Upfront":
|
|
1994
|
+
return "no_upfront";
|
|
1995
|
+
case "All Upfront":
|
|
1996
|
+
return "all_upfront";
|
|
1997
|
+
case "Partial Upfront":
|
|
1998
|
+
return "partial_upfront";
|
|
1999
|
+
default:
|
|
2000
|
+
return null;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// src/pricing/azure/azure-normalizer.ts
|
|
2005
|
+
function normalize2(item, spec) {
|
|
2006
|
+
return {
|
|
2007
|
+
provider: "azure",
|
|
2008
|
+
service: spec.service,
|
|
2009
|
+
resource_type: item.skuName ?? item.armSkuName ?? "unknown",
|
|
2010
|
+
region: item.armRegionName ?? item.location ?? "",
|
|
2011
|
+
unit: item.unitOfMeasure ?? spec.defaultUnit,
|
|
2012
|
+
price_per_unit: item.retailPrice ?? item.unitPrice ?? 0,
|
|
2013
|
+
currency: item.currencyCode ?? "USD",
|
|
2014
|
+
tier: spec.includeTier && item.tierMinimumUnits !== void 0 ? String(item.tierMinimumUnits) : void 0,
|
|
2015
|
+
description: item.productName ?? void 0,
|
|
2016
|
+
attributes: {
|
|
2017
|
+
sku_name: item.skuName ?? "",
|
|
2018
|
+
...spec.extraAttributes?.(item),
|
|
2019
|
+
service_name: item.serviceName ?? spec.defaultServiceName ?? "",
|
|
2020
|
+
product_name: item.productName ?? "",
|
|
2021
|
+
meter_name: item.meterName ?? "",
|
|
1622
2022
|
pricing_source: "live"
|
|
1623
2023
|
},
|
|
1624
|
-
effective_date: item.effectiveStartDate
|
|
2024
|
+
effective_date: resolveEffectiveDate(item.effectiveStartDate)
|
|
1625
2025
|
};
|
|
1626
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
|
+
}
|
|
2036
|
+
function normalizeAzureDatabase(item) {
|
|
2037
|
+
return normalize2(item, {
|
|
2038
|
+
service: "azure-database",
|
|
2039
|
+
defaultUnit: "1 Hour"
|
|
2040
|
+
});
|
|
2041
|
+
}
|
|
2042
|
+
function normalizeAzureStorage(item) {
|
|
2043
|
+
return normalize2(item, {
|
|
2044
|
+
service: "managed-disks",
|
|
2045
|
+
defaultUnit: "1 GiB/Month"
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
// src/pricing/azure/arm-regions.ts
|
|
2050
|
+
var ARM_REGION_MAP = {
|
|
2051
|
+
// Americas
|
|
2052
|
+
eastus: "eastus",
|
|
2053
|
+
eastus2: "eastus2",
|
|
2054
|
+
eastus3: "eastus3",
|
|
2055
|
+
southcentralus: "southcentralus",
|
|
2056
|
+
westus: "westus",
|
|
2057
|
+
westus2: "westus2",
|
|
2058
|
+
westus3: "westus3",
|
|
2059
|
+
centralus: "centralus",
|
|
2060
|
+
northcentralus: "northcentralus",
|
|
2061
|
+
westcentralus: "westcentralus",
|
|
2062
|
+
canadacentral: "canadacentral",
|
|
2063
|
+
canadaeast: "canadaeast",
|
|
2064
|
+
brazilsouth: "brazilsouth",
|
|
2065
|
+
brazilsoutheast: "brazilsoutheast",
|
|
2066
|
+
mexicocentral: "mexicocentral",
|
|
2067
|
+
// Europe
|
|
2068
|
+
northeurope: "northeurope",
|
|
2069
|
+
westeurope: "westeurope",
|
|
2070
|
+
uksouth: "uksouth",
|
|
2071
|
+
ukwest: "ukwest",
|
|
2072
|
+
francecentral: "francecentral",
|
|
2073
|
+
francesouth: "francesouth",
|
|
2074
|
+
germanywestcentral: "germanywestcentral",
|
|
2075
|
+
germanynorth: "germanynorth",
|
|
2076
|
+
switzerlandnorth: "switzerlandnorth",
|
|
2077
|
+
switzerlandwest: "switzerlandwest",
|
|
2078
|
+
norwayeast: "norwayeast",
|
|
2079
|
+
norwaywest: "norwaywest",
|
|
2080
|
+
swedencentral: "swedencentral",
|
|
2081
|
+
swedensouth: "swedensouth",
|
|
2082
|
+
polandcentral: "polandcentral",
|
|
2083
|
+
italynorth: "italynorth",
|
|
2084
|
+
spaincentral: "spaincentral",
|
|
2085
|
+
// Asia Pacific
|
|
2086
|
+
eastasia: "eastasia",
|
|
2087
|
+
southeastasia: "southeastasia",
|
|
2088
|
+
japaneast: "japaneast",
|
|
2089
|
+
japanwest: "japanwest",
|
|
2090
|
+
australiaeast: "australiaeast",
|
|
2091
|
+
australiasoutheast: "australiasoutheast",
|
|
2092
|
+
australiacentral: "australiacentral",
|
|
2093
|
+
australiacentral2: "australiacentral2",
|
|
2094
|
+
centralindia: "centralindia",
|
|
2095
|
+
southindia: "southindia",
|
|
2096
|
+
westindia: "westindia",
|
|
2097
|
+
jioindiawest: "jioindiawest",
|
|
2098
|
+
jioindiacentral: "jioindiacentral",
|
|
2099
|
+
koreacentral: "koreacentral",
|
|
2100
|
+
koreasouth: "koreasouth",
|
|
2101
|
+
// Middle East / Africa
|
|
2102
|
+
uaenorth: "uaenorth",
|
|
2103
|
+
uaecentral: "uaecentral",
|
|
2104
|
+
qatarcentral: "qatarcentral",
|
|
2105
|
+
israelcentral: "israelcentral",
|
|
2106
|
+
southafricanorth: "southafricanorth",
|
|
2107
|
+
southafricawest: "southafricawest"
|
|
2108
|
+
};
|
|
2109
|
+
var ALIASES = {
|
|
2110
|
+
// Americas
|
|
2111
|
+
eastus: "eastus",
|
|
2112
|
+
eastus2: "eastus2",
|
|
2113
|
+
eastus3: "eastus3",
|
|
2114
|
+
southcentralus: "southcentralus",
|
|
2115
|
+
westus: "westus",
|
|
2116
|
+
westus2: "westus2",
|
|
2117
|
+
westus3: "westus3",
|
|
2118
|
+
centralus: "centralus",
|
|
2119
|
+
northcentralus: "northcentralus",
|
|
2120
|
+
westcentralus: "westcentralus",
|
|
2121
|
+
canadacentral: "canadacentral",
|
|
2122
|
+
canadaeast: "canadaeast",
|
|
2123
|
+
brazilsouth: "brazilsouth",
|
|
2124
|
+
brazilsoutheast: "brazilsoutheast",
|
|
2125
|
+
mexicocentral: "mexicocentral",
|
|
2126
|
+
// Europe
|
|
2127
|
+
northeurope: "northeurope",
|
|
2128
|
+
westeurope: "westeurope",
|
|
2129
|
+
uksouth: "uksouth",
|
|
2130
|
+
ukwest: "ukwest",
|
|
2131
|
+
francecentral: "francecentral",
|
|
2132
|
+
francesouth: "francesouth",
|
|
2133
|
+
germanywestcentral: "germanywestcentral",
|
|
2134
|
+
germanynorth: "germanynorth",
|
|
2135
|
+
switzerlandnorth: "switzerlandnorth",
|
|
2136
|
+
switzerlandwest: "switzerlandwest",
|
|
2137
|
+
norwayeast: "norwayeast",
|
|
2138
|
+
norwaywest: "norwaywest",
|
|
2139
|
+
swedencentral: "swedencentral",
|
|
2140
|
+
swedensouth: "swedensouth",
|
|
2141
|
+
polandcentral: "polandcentral",
|
|
2142
|
+
italynorth: "italynorth",
|
|
2143
|
+
spaincentral: "spaincentral",
|
|
2144
|
+
// Asia Pacific
|
|
2145
|
+
eastasia: "eastasia",
|
|
2146
|
+
southeastasia: "southeastasia",
|
|
2147
|
+
japaneast: "japaneast",
|
|
2148
|
+
japanwest: "japanwest",
|
|
2149
|
+
australiaeast: "australiaeast",
|
|
2150
|
+
australiasoutheast: "australiasoutheast",
|
|
2151
|
+
australiacentral: "australiacentral",
|
|
2152
|
+
australiacentral2: "australiacentral2",
|
|
2153
|
+
centralindia: "centralindia",
|
|
2154
|
+
southindia: "southindia",
|
|
2155
|
+
westindia: "westindia",
|
|
2156
|
+
jioindiawest: "jioindiawest",
|
|
2157
|
+
jioindiacentral: "jioindiacentral",
|
|
2158
|
+
koreacentral: "koreacentral",
|
|
2159
|
+
koreasouth: "koreasouth",
|
|
2160
|
+
// Middle East / Africa
|
|
2161
|
+
uaenorth: "uaenorth",
|
|
2162
|
+
uaecentral: "uaecentral",
|
|
2163
|
+
qatarcentral: "qatarcentral",
|
|
2164
|
+
israelcentral: "israelcentral",
|
|
2165
|
+
southafricanorth: "southafricanorth",
|
|
2166
|
+
southafricawest: "southafricawest"
|
|
2167
|
+
};
|
|
2168
|
+
function toArmRegionName(region) {
|
|
2169
|
+
if (!region) return region;
|
|
2170
|
+
const key = region.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2171
|
+
return ALIASES[key] ?? ARM_REGION_MAP[key] ?? key;
|
|
2172
|
+
}
|
|
1627
2173
|
|
|
1628
2174
|
// src/pricing/azure/fallback-data.ts
|
|
1629
2175
|
var VM_BASE_PRICES = {
|
|
@@ -1655,27 +2201,26 @@ var VM_BASE_PRICES = {
|
|
|
1655
2201
|
standard_e32s_v5: 2.016,
|
|
1656
2202
|
standard_e2ds_v5: 0.144,
|
|
1657
2203
|
standard_e4ds_v5: 0.288,
|
|
1658
|
-
standard_f2s_v2: 0.
|
|
1659
|
-
standard_f4s_v2: 0.
|
|
1660
|
-
standard_f8s_v2: 0.
|
|
1661
|
-
standard_f16s_v2: 0.
|
|
1662
|
-
standard_f32s_v2: 1.
|
|
1663
|
-
standard_l8s_v3: 0.
|
|
1664
|
-
standard_l16s_v3: 1.
|
|
2204
|
+
standard_f2s_v2: 0.0846,
|
|
2205
|
+
standard_f4s_v2: 0.169,
|
|
2206
|
+
standard_f8s_v2: 0.338,
|
|
2207
|
+
standard_f16s_v2: 0.677,
|
|
2208
|
+
standard_f32s_v2: 1.353,
|
|
2209
|
+
standard_l8s_v3: 0.696,
|
|
2210
|
+
standard_l16s_v3: 1.392,
|
|
1665
2211
|
standard_nc4as_t4_v3: 0.526,
|
|
1666
2212
|
standard_nc8as_t4_v3: 0.752,
|
|
1667
2213
|
standard_e2ps_v5: 0.101,
|
|
1668
2214
|
standard_e4ps_v5: 0.202,
|
|
1669
2215
|
standard_e8ps_v5: 0.403,
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
standard_e16s_v6: 1.008
|
|
2216
|
+
standard_d2s_v6: 0.101,
|
|
2217
|
+
standard_d4s_v6: 0.202,
|
|
2218
|
+
standard_d8s_v6: 0.403,
|
|
2219
|
+
standard_d16s_v6: 0.806,
|
|
2220
|
+
standard_e2s_v6: 0.132,
|
|
2221
|
+
standard_e4s_v6: 0.265,
|
|
2222
|
+
standard_e8s_v6: 0.529,
|
|
2223
|
+
standard_e16s_v6: 1.058
|
|
1679
2224
|
};
|
|
1680
2225
|
var DISK_BASE_PRICES = {
|
|
1681
2226
|
premium_lrs: 0.132,
|
|
@@ -1710,21 +2255,15 @@ var NAT_PER_GB2 = 0.045;
|
|
|
1710
2255
|
var AKS_HOURLY = 0.1;
|
|
1711
2256
|
var CACHE_TTL2 = 86400;
|
|
1712
2257
|
var RETAIL_API_BASE = "https://prices.azure.com/api/retail/prices";
|
|
1713
|
-
function regionMultiplier2(region) {
|
|
1714
|
-
const multipliers = getRegionPriceMultipliers();
|
|
1715
|
-
const mult = multipliers.azure[region.toLowerCase()];
|
|
1716
|
-
if (mult === void 0) {
|
|
1717
|
-
logger.warn("region-multiplier: unknown Azure region, defaulting to 1.0x", { region });
|
|
1718
|
-
return 1;
|
|
1719
|
-
}
|
|
1720
|
-
return mult;
|
|
1721
|
-
}
|
|
1722
2258
|
|
|
1723
2259
|
// src/pricing/azure/retail-client.ts
|
|
1724
2260
|
var AzureRetailClient = class {
|
|
1725
2261
|
cache;
|
|
1726
|
-
|
|
2262
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
2263
|
+
ttlSeconds;
|
|
2264
|
+
constructor(cache, ttlSeconds = CACHE_TTL2) {
|
|
1727
2265
|
this.cache = cache;
|
|
2266
|
+
this.ttlSeconds = ttlSeconds;
|
|
1728
2267
|
}
|
|
1729
2268
|
// -------------------------------------------------------------------------
|
|
1730
2269
|
// Public API
|
|
@@ -1734,13 +2273,13 @@ var AzureRetailClient = class {
|
|
|
1734
2273
|
const cached = this.cache.get(cacheKey);
|
|
1735
2274
|
if (cached) return cached;
|
|
1736
2275
|
try {
|
|
1737
|
-
const armRegion = region
|
|
2276
|
+
const armRegion = toArmRegionName(region);
|
|
1738
2277
|
const filter = this.buildODataFilter("Virtual Machines", armRegion, vmSize, true);
|
|
1739
2278
|
const items = await this.queryPricing(filter);
|
|
1740
2279
|
const match = this.pickVmItem(items, vmSize, os);
|
|
1741
2280
|
if (match) {
|
|
1742
2281
|
const result = normalizeAzureCompute(match);
|
|
1743
|
-
this.cache.set(cacheKey, result, "azure", "vm", region,
|
|
2282
|
+
this.cache.set(cacheKey, result, "azure", "vm", region, this.ttlSeconds);
|
|
1744
2283
|
return result;
|
|
1745
2284
|
}
|
|
1746
2285
|
} catch (err) {
|
|
@@ -1757,11 +2296,11 @@ var AzureRetailClient = class {
|
|
|
1757
2296
|
const cached = this.cache.get(cacheKey);
|
|
1758
2297
|
if (cached) return cached;
|
|
1759
2298
|
try {
|
|
1760
|
-
const armRegion = region
|
|
2299
|
+
const armRegion = toArmRegionName(region);
|
|
1761
2300
|
const serviceName = `Azure Database for ${engine}`;
|
|
1762
2301
|
const result = await this.queryDatabasePrice(tier, armRegion, serviceName);
|
|
1763
2302
|
if (result) {
|
|
1764
|
-
this.cache.set(cacheKey, result, "azure", "db", region,
|
|
2303
|
+
this.cache.set(cacheKey, result, "azure", "db", region, this.ttlSeconds);
|
|
1765
2304
|
return result;
|
|
1766
2305
|
}
|
|
1767
2306
|
logger.warn("Azure Retail API returned no matching DB pricing", {
|
|
@@ -1816,28 +2355,34 @@ var AzureRetailClient = class {
|
|
|
1816
2355
|
/**
|
|
1817
2356
|
* Parse a VM name like "Standard_D2s_v3" into series info.
|
|
1818
2357
|
* Returns { series: "Dsv3", vcpus: 2 } or null if unparseable.
|
|
2358
|
+
*
|
|
2359
|
+
* Real-world Azure SKUs have a lot of shape variation we have to handle:
|
|
2360
|
+
* Standard_D2s_v5 – 1-letter family, lower-case variant
|
|
2361
|
+
* Standard_M64ms_v2 – multi-digit vCPU count
|
|
2362
|
+
* Standard_DC4s_v3 – 2-letter family (confidential compute)
|
|
2363
|
+
* Standard_E96ias_v5 – longer (3-letter) variant
|
|
2364
|
+
* Standard_NC4as_T4_v3 – mid-name accelerator segment (T4)
|
|
2365
|
+
* Standard_Eb8as_v5 – single-letter "sub-family" prefix (b)
|
|
2366
|
+
*
|
|
2367
|
+
* The regex below accepts:
|
|
2368
|
+
* <FAMILY 1-2 letters><optional sub-family letter><vCPU digits>
|
|
2369
|
+
* <variant letters><optional _SEG segments like _T4>_v<version digits>
|
|
1819
2370
|
*/
|
|
1820
2371
|
parseVmSeries(vmName) {
|
|
1821
|
-
|
|
1822
|
-
if (!match) return null;
|
|
1823
|
-
const [, family, vcpuStr, variant, version] = match;
|
|
1824
|
-
const vcpus = parseInt(vcpuStr, 10);
|
|
1825
|
-
if (isNaN(vcpus) || vcpus === 0) return null;
|
|
1826
|
-
const series = `${family}${variant}v${version}`;
|
|
1827
|
-
return { series, vcpus };
|
|
2372
|
+
return parseAzureVmSeries(vmName);
|
|
1828
2373
|
}
|
|
1829
2374
|
async getStoragePrice(diskType, region) {
|
|
1830
2375
|
const cacheKey = this.buildCacheKey("disk", region, diskType);
|
|
1831
2376
|
const cached = this.cache.get(cacheKey);
|
|
1832
2377
|
if (cached) return cached;
|
|
1833
2378
|
try {
|
|
1834
|
-
const armRegion = region
|
|
2379
|
+
const armRegion = toArmRegionName(region);
|
|
1835
2380
|
const filter = this.buildODataFilter("Storage", armRegion, diskType);
|
|
1836
2381
|
const items = await this.queryPricing(filter);
|
|
1837
2382
|
if (items.length > 0) {
|
|
1838
2383
|
items.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
1839
2384
|
const result = normalizeAzureStorage(items[0]);
|
|
1840
|
-
this.cache.set(cacheKey, result, "azure", "storage", region,
|
|
2385
|
+
this.cache.set(cacheKey, result, "azure", "storage", region, this.ttlSeconds);
|
|
1841
2386
|
return result;
|
|
1842
2387
|
}
|
|
1843
2388
|
logger.warn("Azure Retail API returned no matching storage pricing", {
|
|
@@ -1853,9 +2398,9 @@ var AzureRetailClient = class {
|
|
|
1853
2398
|
}
|
|
1854
2399
|
return this.fallbackStoragePrice(diskType, region, cacheKey);
|
|
1855
2400
|
}
|
|
1856
|
-
|
|
1857
|
-
const multiplier =
|
|
1858
|
-
return {
|
|
2401
|
+
getLoadBalancerPrice(region) {
|
|
2402
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2403
|
+
return Promise.resolve({
|
|
1859
2404
|
provider: "azure",
|
|
1860
2405
|
service: "load-balancer",
|
|
1861
2406
|
resource_type: "standard",
|
|
@@ -1869,11 +2414,11 @@ var AzureRetailClient = class {
|
|
|
1869
2414
|
pricing_model: "fixed_plus_rules"
|
|
1870
2415
|
},
|
|
1871
2416
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1872
|
-
};
|
|
2417
|
+
});
|
|
1873
2418
|
}
|
|
1874
|
-
|
|
1875
|
-
const multiplier =
|
|
1876
|
-
return {
|
|
2419
|
+
getNatGatewayPrice(region) {
|
|
2420
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2421
|
+
return Promise.resolve({
|
|
1877
2422
|
provider: "azure",
|
|
1878
2423
|
service: "nat-gateway",
|
|
1879
2424
|
resource_type: "nat-gateway",
|
|
@@ -1886,11 +2431,11 @@ var AzureRetailClient = class {
|
|
|
1886
2431
|
per_gb_price: String(NAT_PER_GB2 * multiplier)
|
|
1887
2432
|
},
|
|
1888
2433
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1889
|
-
};
|
|
2434
|
+
});
|
|
1890
2435
|
}
|
|
1891
|
-
|
|
1892
|
-
const multiplier =
|
|
1893
|
-
return {
|
|
2436
|
+
getKubernetesPrice(region) {
|
|
2437
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2438
|
+
return Promise.resolve({
|
|
1894
2439
|
provider: "azure",
|
|
1895
2440
|
service: "aks",
|
|
1896
2441
|
resource_type: "cluster",
|
|
@@ -1901,7 +2446,113 @@ var AzureRetailClient = class {
|
|
|
1901
2446
|
description: "Azure Kubernetes Service (uptime SLA tier, per cluster/hour)",
|
|
1902
2447
|
attributes: {},
|
|
1903
2448
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
1904
|
-
};
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Fetch the live Spot VM price for a given VM size/region/OS from the Azure
|
|
2453
|
+
* Retail Prices API. Spot VMs are exposed as distinct rows where the
|
|
2454
|
+
* `meterName` contains "Spot" (e.g. "D2s v5 Spot"). Returns `null` when no
|
|
2455
|
+
* spot row is found — callers should fall back to static discount factors.
|
|
2456
|
+
*
|
|
2457
|
+
* Cache key is namespaced under "vm-spot" so spot and on-demand entries
|
|
2458
|
+
* don't collide.
|
|
2459
|
+
*/
|
|
2460
|
+
async getSpotPrice(vmSize, region, os = "linux") {
|
|
2461
|
+
const cacheKey = this.buildCacheKey("vm-spot", region, vmSize, os);
|
|
2462
|
+
const cached = this.cache.get(cacheKey);
|
|
2463
|
+
if (cached) return cached;
|
|
2464
|
+
try {
|
|
2465
|
+
const armRegion = toArmRegionName(region);
|
|
2466
|
+
const filter = this.buildODataFilter("Virtual Machines", armRegion, vmSize, true);
|
|
2467
|
+
const items = await this.queryPricing(filter);
|
|
2468
|
+
const isWindows = os.toLowerCase().includes("windows");
|
|
2469
|
+
const spotItems = items.filter((i) => {
|
|
2470
|
+
const meter = (i.meterName ?? "").toLowerCase();
|
|
2471
|
+
const sku = (i.skuName ?? "").toLowerCase();
|
|
2472
|
+
const hasWindows = sku.includes("windows") || meter.includes("windows");
|
|
2473
|
+
const isSpot = meter.includes("spot") || sku.includes("spot");
|
|
2474
|
+
return isSpot && (isWindows && hasWindows || !isWindows && !hasWindows);
|
|
2475
|
+
});
|
|
2476
|
+
if (spotItems.length > 0) {
|
|
2477
|
+
spotItems.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
2478
|
+
const result = normalizeAzureCompute(spotItems[0]);
|
|
2479
|
+
result.attributes.pricing_source = "live";
|
|
2480
|
+
result.attributes.purchase_option = "spot";
|
|
2481
|
+
this.cache.set(cacheKey, result, "azure", "vm-spot", region, this.ttlSeconds);
|
|
2482
|
+
return result;
|
|
2483
|
+
}
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
logger.warn("Azure Retail API Spot fetch failed", {
|
|
2486
|
+
region,
|
|
2487
|
+
vmSize,
|
|
2488
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2489
|
+
});
|
|
2490
|
+
}
|
|
2491
|
+
return null;
|
|
2492
|
+
}
|
|
2493
|
+
/**
|
|
2494
|
+
* Fetch the live Reservation price for a VM size/region/term from the Azure
|
|
2495
|
+
* Retail Prices API. The API exposes reservations via `priceType eq
|
|
2496
|
+
* 'Reservation'`, with `reservationTerm` of "1 Year" or "3 Years". Returns
|
|
2497
|
+
* `null` when no reservation row matches — callers should fall back to
|
|
2498
|
+
* static discount factors.
|
|
2499
|
+
*
|
|
2500
|
+
* The returned NormalizedPrice uses the reservation's `unitOfMeasure`
|
|
2501
|
+
* (typically "1 Hour" after Azure's per-hour amortisation but sometimes
|
|
2502
|
+
* "1/Year" for upfront payment). The `retailPrice` is the total reservation
|
|
2503
|
+
* price as returned — callers are expected to convert to an hourly rate
|
|
2504
|
+
* themselves using the unit.
|
|
2505
|
+
*/
|
|
2506
|
+
async getReservationPrice(vmSize, region, term) {
|
|
2507
|
+
const cacheKey = this.buildCacheKey("vm-ri", region, vmSize, term);
|
|
2508
|
+
const cached = this.cache.get(cacheKey);
|
|
2509
|
+
if (cached) return cached;
|
|
2510
|
+
try {
|
|
2511
|
+
const armRegion = toArmRegionName(region);
|
|
2512
|
+
const termLabel = term === "1yr" ? "1 Year" : "3 Years";
|
|
2513
|
+
const filter = `serviceName eq 'Virtual Machines' and armRegionName eq '${armRegion}' and priceType eq 'Reservation' and armSkuName eq '${vmSize}'`;
|
|
2514
|
+
const items = await this.queryPricing(filter);
|
|
2515
|
+
const matches = items.filter((i) => (i.reservationTerm ?? "") === termLabel);
|
|
2516
|
+
if (matches.length > 0) {
|
|
2517
|
+
matches.sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
2518
|
+
const result = normalizeAzureCompute(matches[0]);
|
|
2519
|
+
result.attributes.pricing_source = "live";
|
|
2520
|
+
result.attributes.purchase_option = "reservation";
|
|
2521
|
+
result.attributes.reservation_term = termLabel;
|
|
2522
|
+
this.cache.set(cacheKey, result, "azure", "vm-ri", region, this.ttlSeconds);
|
|
2523
|
+
return result;
|
|
2524
|
+
}
|
|
2525
|
+
} catch (err) {
|
|
2526
|
+
logger.warn("Azure Retail API Reservation fetch failed", {
|
|
2527
|
+
region,
|
|
2528
|
+
vmSize,
|
|
2529
|
+
term,
|
|
2530
|
+
err: err instanceof Error ? err.message : String(err)
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
return null;
|
|
2534
|
+
}
|
|
2535
|
+
/**
|
|
2536
|
+
* Convenience helper: given a VM size and region, return the effective
|
|
2537
|
+
* *hourly* reservation rate (amortised). Returns `null` when the live API
|
|
2538
|
+
* has no matching reservation. This wraps `getReservationPrice` and
|
|
2539
|
+
* converts "1/Year" / "1/3 Years" billed units into an hourly equivalent
|
|
2540
|
+
* using a 730h/month * 12 / 36 month assumption.
|
|
2541
|
+
*/
|
|
2542
|
+
async getReservationHourlyRate(vmSize, region, term) {
|
|
2543
|
+
const price = await this.getReservationPrice(vmSize, region, term);
|
|
2544
|
+
if (!price) return null;
|
|
2545
|
+
const unit = (price.unit ?? "").toLowerCase();
|
|
2546
|
+
const raw = price.price_per_unit;
|
|
2547
|
+
if (!isFinite(raw) || raw <= 0) return null;
|
|
2548
|
+
if (unit.includes("hour")) {
|
|
2549
|
+
return raw;
|
|
2550
|
+
}
|
|
2551
|
+
if (unit.includes("year")) {
|
|
2552
|
+
const years = term === "1yr" ? 1 : 3;
|
|
2553
|
+
return raw / (8760 * years);
|
|
2554
|
+
}
|
|
2555
|
+
return raw;
|
|
1905
2556
|
}
|
|
1906
2557
|
// -------------------------------------------------------------------------
|
|
1907
2558
|
// Internal helpers – live API
|
|
@@ -1927,8 +2578,8 @@ var AzureRetailClient = class {
|
|
|
1927
2578
|
}
|
|
1928
2579
|
return allItems;
|
|
1929
2580
|
}
|
|
1930
|
-
buildODataFilter(service, armRegion, skuName, exactArmSku) {
|
|
1931
|
-
let filter = `serviceName eq '${service}' and armRegionName eq '${armRegion}' and priceType eq '
|
|
2581
|
+
buildODataFilter(service, armRegion, skuName, exactArmSku, priceType = "Consumption") {
|
|
2582
|
+
let filter = `serviceName eq '${service}' and armRegionName eq '${armRegion}' and priceType eq '${priceType}'`;
|
|
1932
2583
|
if (skuName) {
|
|
1933
2584
|
if (exactArmSku) {
|
|
1934
2585
|
filter += ` and armSkuName eq '${skuName}'`;
|
|
@@ -1949,16 +2600,20 @@ var AzureRetailClient = class {
|
|
|
1949
2600
|
pickVmItem(items, vmSize, os) {
|
|
1950
2601
|
if (items.length === 0) return null;
|
|
1951
2602
|
const sorted = [...items].sort((a, b) => (a.skuId ?? "").localeCompare(b.skuId ?? ""));
|
|
1952
|
-
const
|
|
2603
|
+
const skuFragment = vmSize.toLowerCase().replace(/^standard_/, "").replace(/_/g, " ");
|
|
1953
2604
|
const isWindows = os.toLowerCase().includes("windows");
|
|
1954
|
-
|
|
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) {
|
|
1955
2610
|
const skuLower = (item.skuName ?? "").toLowerCase();
|
|
1956
|
-
const hasWindows = skuLower.includes("windows");
|
|
1957
|
-
if (skuLower.includes(
|
|
2611
|
+
const hasWindows = skuLower.includes("windows") || (item.productName ?? "").toLowerCase().includes("windows");
|
|
2612
|
+
if (skuLower.includes(skuFragment) && (isWindows && hasWindows || !isWindows && !hasWindows)) {
|
|
1958
2613
|
return item;
|
|
1959
2614
|
}
|
|
1960
2615
|
}
|
|
1961
|
-
return sorted[0];
|
|
2616
|
+
return onDemand[0] ?? sorted[0];
|
|
1962
2617
|
}
|
|
1963
2618
|
// -------------------------------------------------------------------------
|
|
1964
2619
|
// Internal helpers – size interpolation
|
|
@@ -1998,7 +2653,7 @@ var AzureRetailClient = class {
|
|
|
1998
2653
|
logger.warn("No fallback price found for Azure VM size", { vmSize });
|
|
1999
2654
|
return null;
|
|
2000
2655
|
}
|
|
2001
|
-
const multiplier =
|
|
2656
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2002
2657
|
const result = {
|
|
2003
2658
|
provider: "azure",
|
|
2004
2659
|
service: "virtual-machines",
|
|
@@ -2015,7 +2670,7 @@ var AzureRetailClient = class {
|
|
|
2015
2670
|
},
|
|
2016
2671
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2017
2672
|
};
|
|
2018
|
-
this.cache.set(cacheKey, result, "azure", "vm", region,
|
|
2673
|
+
this.cache.set(cacheKey, result, "azure", "vm", region, this.ttlSeconds);
|
|
2019
2674
|
return result;
|
|
2020
2675
|
}
|
|
2021
2676
|
fallbackDatabasePrice(tier, region, engine, cacheKey) {
|
|
@@ -2025,7 +2680,7 @@ var AzureRetailClient = class {
|
|
|
2025
2680
|
logger.warn("No fallback price found for Azure DB tier", { tier });
|
|
2026
2681
|
return null;
|
|
2027
2682
|
}
|
|
2028
|
-
const multiplier =
|
|
2683
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2029
2684
|
const result = {
|
|
2030
2685
|
provider: "azure",
|
|
2031
2686
|
service: "azure-database",
|
|
@@ -2042,7 +2697,7 @@ var AzureRetailClient = class {
|
|
|
2042
2697
|
},
|
|
2043
2698
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2044
2699
|
};
|
|
2045
|
-
this.cache.set(cacheKey, result, "azure", "db", region,
|
|
2700
|
+
this.cache.set(cacheKey, result, "azure", "db", region, this.ttlSeconds);
|
|
2046
2701
|
return result;
|
|
2047
2702
|
}
|
|
2048
2703
|
fallbackStoragePrice(diskType, region, cacheKey) {
|
|
@@ -2052,7 +2707,7 @@ var AzureRetailClient = class {
|
|
|
2052
2707
|
logger.warn("No fallback price found for Azure disk type", { diskType });
|
|
2053
2708
|
return null;
|
|
2054
2709
|
}
|
|
2055
|
-
const multiplier =
|
|
2710
|
+
const multiplier = regionMultiplier("azure", region);
|
|
2056
2711
|
const result = {
|
|
2057
2712
|
provider: "azure",
|
|
2058
2713
|
service: "managed-disks",
|
|
@@ -2068,79 +2723,77 @@ var AzureRetailClient = class {
|
|
|
2068
2723
|
},
|
|
2069
2724
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2070
2725
|
};
|
|
2071
|
-
this.cache.set(cacheKey, result, "azure", "storage", region,
|
|
2726
|
+
this.cache.set(cacheKey, result, "azure", "storage", region, this.ttlSeconds);
|
|
2072
2727
|
return result;
|
|
2073
2728
|
}
|
|
2074
2729
|
};
|
|
2730
|
+
function parseAzureVmSeries(vmName) {
|
|
2731
|
+
const match = vmName.match(
|
|
2732
|
+
/^Standard_([A-Z]{1,2})([a-z]?)(\d+)([a-z]*)((?:_[A-Za-z0-9]+)*)_v(\d+)$/i
|
|
2733
|
+
);
|
|
2734
|
+
if (!match) return null;
|
|
2735
|
+
const [, family, subFamily, vcpuStr, variant, extras, version] = match;
|
|
2736
|
+
const vcpus = parseInt(vcpuStr, 10);
|
|
2737
|
+
if (isNaN(vcpus) || vcpus === 0) return null;
|
|
2738
|
+
const extrasJoined = (extras ?? "").replace(/_/g, "");
|
|
2739
|
+
const series = `${family}${subFamily}${variant}${extrasJoined}v${version}`;
|
|
2740
|
+
return { series, vcpus };
|
|
2741
|
+
}
|
|
2075
2742
|
|
|
2076
2743
|
// src/pricing/gcp/gcp-normalizer.ts
|
|
2077
|
-
function
|
|
2744
|
+
function normalize3(resourceType, pricePerUnit, region, spec, opts) {
|
|
2078
2745
|
return {
|
|
2079
2746
|
provider: "gcp",
|
|
2080
|
-
service:
|
|
2081
|
-
resource_type:
|
|
2747
|
+
service: spec.service,
|
|
2748
|
+
resource_type: resourceType,
|
|
2082
2749
|
region,
|
|
2083
|
-
unit:
|
|
2084
|
-
price_per_unit:
|
|
2750
|
+
unit: spec.unit,
|
|
2751
|
+
price_per_unit: pricePerUnit,
|
|
2085
2752
|
currency: "USD",
|
|
2086
|
-
description:
|
|
2753
|
+
description: `${spec.descriptionPrefix} ${resourceType}`,
|
|
2087
2754
|
attributes: {
|
|
2088
|
-
|
|
2089
|
-
|
|
2755
|
+
[spec.attrKey]: resourceType,
|
|
2756
|
+
...opts.skuId !== void 0 ? { sku_id: opts.skuId } : {},
|
|
2757
|
+
pricing_source: opts.source
|
|
2090
2758
|
},
|
|
2091
|
-
effective_date: (
|
|
2759
|
+
effective_date: resolveEffectiveDate(opts.effectiveDate)
|
|
2092
2760
|
};
|
|
2093
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
|
+
}
|
|
2094
2789
|
function normalizeGcpDatabase(tier, hourlyPrice, region) {
|
|
2095
|
-
return {
|
|
2096
|
-
provider: "gcp",
|
|
2097
|
-
service: "cloud-sql",
|
|
2098
|
-
resource_type: tier,
|
|
2099
|
-
region,
|
|
2100
|
-
unit: "h",
|
|
2101
|
-
price_per_unit: hourlyPrice,
|
|
2102
|
-
currency: "USD",
|
|
2103
|
-
description: `GCP Cloud SQL ${tier}`,
|
|
2104
|
-
attributes: {
|
|
2105
|
-
tier,
|
|
2106
|
-
pricing_source: "bundled"
|
|
2107
|
-
},
|
|
2108
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2109
|
-
};
|
|
2790
|
+
return normalize3(tier, hourlyPrice, region, DATABASE_SPEC, { source: "bundled" });
|
|
2110
2791
|
}
|
|
2111
2792
|
function normalizeGcpStorage(storageClass, pricePerGb, region) {
|
|
2112
|
-
return {
|
|
2113
|
-
provider: "gcp",
|
|
2114
|
-
service: "cloud-storage",
|
|
2115
|
-
resource_type: storageClass,
|
|
2116
|
-
region,
|
|
2117
|
-
unit: "GiBy.mo",
|
|
2118
|
-
price_per_unit: pricePerGb,
|
|
2119
|
-
currency: "USD",
|
|
2120
|
-
description: `GCP Cloud Storage ${storageClass}`,
|
|
2121
|
-
attributes: {
|
|
2122
|
-
storage_class: storageClass,
|
|
2123
|
-
pricing_source: "bundled"
|
|
2124
|
-
},
|
|
2125
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2126
|
-
};
|
|
2793
|
+
return normalize3(storageClass, pricePerGb, region, STORAGE_SPEC, { source: "bundled" });
|
|
2127
2794
|
}
|
|
2128
2795
|
function normalizeGcpDisk(diskType, pricePerGb, region) {
|
|
2129
|
-
return {
|
|
2130
|
-
provider: "gcp",
|
|
2131
|
-
service: "persistent-disk",
|
|
2132
|
-
resource_type: diskType,
|
|
2133
|
-
region,
|
|
2134
|
-
unit: "GiBy.mo",
|
|
2135
|
-
price_per_unit: pricePerGb,
|
|
2136
|
-
currency: "USD",
|
|
2137
|
-
description: `GCP Persistent Disk ${diskType}`,
|
|
2138
|
-
attributes: {
|
|
2139
|
-
disk_type: diskType,
|
|
2140
|
-
pricing_source: "bundled"
|
|
2141
|
-
},
|
|
2142
|
-
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2143
|
-
};
|
|
2796
|
+
return normalize3(diskType, pricePerGb, region, DISK_SPEC, { source: "bundled" });
|
|
2144
2797
|
}
|
|
2145
2798
|
|
|
2146
2799
|
// src/pricing/gcp/bundled-loader.ts
|
|
@@ -2150,20 +2803,11 @@ var NAT_HOURLY3 = 0.044;
|
|
|
2150
2803
|
var NAT_PER_GB3 = 0.045;
|
|
2151
2804
|
var GKE_STANDARD_HOURLY = 0.1;
|
|
2152
2805
|
var GKE_AUTOPILOT_VCPU_HOURLY = 0.0445;
|
|
2153
|
-
function regionMultiplier3(region) {
|
|
2154
|
-
const multipliers = getRegionPriceMultipliers();
|
|
2155
|
-
const mult = multipliers.gcp[region.toLowerCase()];
|
|
2156
|
-
if (mult === void 0) {
|
|
2157
|
-
logger.warn("region-multiplier: unknown GCP region, defaulting to 1.0x", { region });
|
|
2158
|
-
return 1;
|
|
2159
|
-
}
|
|
2160
|
-
return mult;
|
|
2161
|
-
}
|
|
2162
2806
|
var GcpBundledLoader = class {
|
|
2163
2807
|
// -------------------------------------------------------------------------
|
|
2164
2808
|
// Public API
|
|
2165
2809
|
// -------------------------------------------------------------------------
|
|
2166
|
-
|
|
2810
|
+
getComputePrice(machineType, region) {
|
|
2167
2811
|
try {
|
|
2168
2812
|
const pricing = getGcpComputePricing();
|
|
2169
2813
|
const regionPrices = pricing[region];
|
|
@@ -2172,7 +2816,7 @@ var GcpBundledLoader = class {
|
|
|
2172
2816
|
region,
|
|
2173
2817
|
machineType
|
|
2174
2818
|
});
|
|
2175
|
-
return null;
|
|
2819
|
+
return Promise.resolve(null);
|
|
2176
2820
|
}
|
|
2177
2821
|
const price = regionPrices[machineType];
|
|
2178
2822
|
if (price === void 0) {
|
|
@@ -2180,19 +2824,19 @@ var GcpBundledLoader = class {
|
|
|
2180
2824
|
region,
|
|
2181
2825
|
machineType
|
|
2182
2826
|
});
|
|
2183
|
-
return null;
|
|
2827
|
+
return Promise.resolve(null);
|
|
2184
2828
|
}
|
|
2185
|
-
return normalizeGcpCompute(machineType, price, region);
|
|
2829
|
+
return Promise.resolve(normalizeGcpCompute(machineType, price, region));
|
|
2186
2830
|
} catch (err) {
|
|
2187
2831
|
logger.error("GCP compute pricing load error", {
|
|
2188
2832
|
machineType,
|
|
2189
2833
|
region,
|
|
2190
2834
|
err: err instanceof Error ? err.message : String(err)
|
|
2191
2835
|
});
|
|
2192
|
-
return null;
|
|
2836
|
+
return Promise.resolve(null);
|
|
2193
2837
|
}
|
|
2194
2838
|
}
|
|
2195
|
-
|
|
2839
|
+
getDatabasePrice(tier, region) {
|
|
2196
2840
|
try {
|
|
2197
2841
|
const pricing = getGcpSqlPricing();
|
|
2198
2842
|
const regionPrices = pricing[region];
|
|
@@ -2201,7 +2845,7 @@ var GcpBundledLoader = class {
|
|
|
2201
2845
|
region,
|
|
2202
2846
|
tier
|
|
2203
2847
|
});
|
|
2204
|
-
return null;
|
|
2848
|
+
return Promise.resolve(null);
|
|
2205
2849
|
}
|
|
2206
2850
|
const price = regionPrices[tier];
|
|
2207
2851
|
if (price === void 0 || tier === "storage_per_gb" || tier === "ha_multiplier") {
|
|
@@ -2209,19 +2853,19 @@ var GcpBundledLoader = class {
|
|
|
2209
2853
|
region,
|
|
2210
2854
|
tier
|
|
2211
2855
|
});
|
|
2212
|
-
return null;
|
|
2856
|
+
return Promise.resolve(null);
|
|
2213
2857
|
}
|
|
2214
|
-
return normalizeGcpDatabase(tier, price, region);
|
|
2858
|
+
return Promise.resolve(normalizeGcpDatabase(tier, price, region));
|
|
2215
2859
|
} catch (err) {
|
|
2216
2860
|
logger.error("GCP SQL pricing load error", {
|
|
2217
2861
|
tier,
|
|
2218
2862
|
region,
|
|
2219
2863
|
err: err instanceof Error ? err.message : String(err)
|
|
2220
2864
|
});
|
|
2221
|
-
return null;
|
|
2865
|
+
return Promise.resolve(null);
|
|
2222
2866
|
}
|
|
2223
2867
|
}
|
|
2224
|
-
|
|
2868
|
+
getStoragePrice(storageClass, region) {
|
|
2225
2869
|
try {
|
|
2226
2870
|
const pricing = getGcpStoragePricing();
|
|
2227
2871
|
const regionPrices = pricing[region];
|
|
@@ -2230,7 +2874,7 @@ var GcpBundledLoader = class {
|
|
|
2230
2874
|
region,
|
|
2231
2875
|
storageClass
|
|
2232
2876
|
});
|
|
2233
|
-
return null;
|
|
2877
|
+
return Promise.resolve(null);
|
|
2234
2878
|
}
|
|
2235
2879
|
const classKey = storageClass.toUpperCase();
|
|
2236
2880
|
const price = regionPrices[classKey];
|
|
@@ -2239,19 +2883,19 @@ var GcpBundledLoader = class {
|
|
|
2239
2883
|
region,
|
|
2240
2884
|
storageClass
|
|
2241
2885
|
});
|
|
2242
|
-
return null;
|
|
2886
|
+
return Promise.resolve(null);
|
|
2243
2887
|
}
|
|
2244
|
-
return normalizeGcpStorage(storageClass.toUpperCase(), price, region);
|
|
2888
|
+
return Promise.resolve(normalizeGcpStorage(storageClass.toUpperCase(), price, region));
|
|
2245
2889
|
} catch (err) {
|
|
2246
2890
|
logger.error("GCP Storage pricing load error", {
|
|
2247
2891
|
storageClass,
|
|
2248
2892
|
region,
|
|
2249
2893
|
err: err instanceof Error ? err.message : String(err)
|
|
2250
2894
|
});
|
|
2251
|
-
return null;
|
|
2895
|
+
return Promise.resolve(null);
|
|
2252
2896
|
}
|
|
2253
2897
|
}
|
|
2254
|
-
|
|
2898
|
+
getDiskPrice(diskType, region) {
|
|
2255
2899
|
try {
|
|
2256
2900
|
const pricing = getGcpDiskPricing();
|
|
2257
2901
|
const regionPrices = pricing[region];
|
|
@@ -2260,7 +2904,7 @@ var GcpBundledLoader = class {
|
|
|
2260
2904
|
region,
|
|
2261
2905
|
diskType
|
|
2262
2906
|
});
|
|
2263
|
-
return null;
|
|
2907
|
+
return Promise.resolve(null);
|
|
2264
2908
|
}
|
|
2265
2909
|
const diskKey = diskType;
|
|
2266
2910
|
const price = regionPrices[diskKey];
|
|
@@ -2269,21 +2913,21 @@ var GcpBundledLoader = class {
|
|
|
2269
2913
|
region,
|
|
2270
2914
|
diskType
|
|
2271
2915
|
});
|
|
2272
|
-
return null;
|
|
2916
|
+
return Promise.resolve(null);
|
|
2273
2917
|
}
|
|
2274
|
-
return normalizeGcpDisk(diskType, price, region);
|
|
2918
|
+
return Promise.resolve(normalizeGcpDisk(diskType, price, region));
|
|
2275
2919
|
} catch (err) {
|
|
2276
2920
|
logger.error("GCP Disk pricing load error", {
|
|
2277
2921
|
diskType,
|
|
2278
2922
|
region,
|
|
2279
2923
|
err: err instanceof Error ? err.message : String(err)
|
|
2280
2924
|
});
|
|
2281
|
-
return null;
|
|
2925
|
+
return Promise.resolve(null);
|
|
2282
2926
|
}
|
|
2283
2927
|
}
|
|
2284
|
-
|
|
2285
|
-
const multiplier =
|
|
2286
|
-
return {
|
|
2928
|
+
getLoadBalancerPrice(region) {
|
|
2929
|
+
const multiplier = regionMultiplier("gcp", region);
|
|
2930
|
+
return Promise.resolve({
|
|
2287
2931
|
provider: "gcp",
|
|
2288
2932
|
service: "cloud-load-balancing",
|
|
2289
2933
|
resource_type: "forwarding-rule",
|
|
@@ -2296,11 +2940,11 @@ var GcpBundledLoader = class {
|
|
|
2296
2940
|
per_gb_price: String(LB_PER_GB * multiplier)
|
|
2297
2941
|
},
|
|
2298
2942
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2299
|
-
};
|
|
2943
|
+
});
|
|
2300
2944
|
}
|
|
2301
|
-
|
|
2302
|
-
const multiplier =
|
|
2303
|
-
return {
|
|
2945
|
+
getNatGatewayPrice(region) {
|
|
2946
|
+
const multiplier = regionMultiplier("gcp", region);
|
|
2947
|
+
return Promise.resolve({
|
|
2304
2948
|
provider: "gcp",
|
|
2305
2949
|
service: "cloud-nat",
|
|
2306
2950
|
resource_type: "nat-gateway",
|
|
@@ -2313,12 +2957,12 @@ var GcpBundledLoader = class {
|
|
|
2313
2957
|
per_gb_price: String(NAT_PER_GB3 * multiplier)
|
|
2314
2958
|
},
|
|
2315
2959
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2316
|
-
};
|
|
2960
|
+
});
|
|
2317
2961
|
}
|
|
2318
|
-
|
|
2319
|
-
const multiplier =
|
|
2962
|
+
getKubernetesPrice(region, mode = "standard") {
|
|
2963
|
+
const multiplier = regionMultiplier("gcp", region);
|
|
2320
2964
|
const baseHourly = mode === "autopilot" ? GKE_AUTOPILOT_VCPU_HOURLY : GKE_STANDARD_HOURLY;
|
|
2321
|
-
return {
|
|
2965
|
+
return Promise.resolve({
|
|
2322
2966
|
provider: "gcp",
|
|
2323
2967
|
service: "gke",
|
|
2324
2968
|
resource_type: "cluster",
|
|
@@ -2335,7 +2979,7 @@ var GcpBundledLoader = class {
|
|
|
2335
2979
|
}
|
|
2336
2980
|
},
|
|
2337
2981
|
effective_date: (/* @__PURE__ */ new Date()).toISOString()
|
|
2338
|
-
};
|
|
2982
|
+
});
|
|
2339
2983
|
}
|
|
2340
2984
|
};
|
|
2341
2985
|
|
|
@@ -2344,7 +2988,9 @@ var BILLING_API_BASE = "https://cloudbilling.googleapis.com/v1/services";
|
|
|
2344
2988
|
var SERVICE_IDS = {
|
|
2345
2989
|
COMPUTE: "6F81-5844-456A",
|
|
2346
2990
|
CLOUD_SQL: "9662-B51E-5089",
|
|
2347
|
-
CLOUD_STORAGE: "95FF-2EF5-5EA1"
|
|
2991
|
+
CLOUD_STORAGE: "95FF-2EF5-5EA1",
|
|
2992
|
+
// GKE (Kubernetes Engine) service — used to locate Autopilot pod SKUs.
|
|
2993
|
+
GKE: "CCD8-9BF1-090E"
|
|
2348
2994
|
};
|
|
2349
2995
|
var CACHE_TTL3 = 86400;
|
|
2350
2996
|
function protoToFloat(units, nanos) {
|
|
@@ -2372,8 +3018,11 @@ function skuCoversRegion(sku, region) {
|
|
|
2372
3018
|
}
|
|
2373
3019
|
var CloudBillingClient = class {
|
|
2374
3020
|
cache;
|
|
2375
|
-
|
|
3021
|
+
/** Cache TTL for pricing writes (seconds); from config.cache.ttl_seconds. */
|
|
3022
|
+
ttlSeconds;
|
|
3023
|
+
constructor(cache, ttlSeconds = CACHE_TTL3) {
|
|
2376
3024
|
this.cache = cache;
|
|
3025
|
+
this.ttlSeconds = ttlSeconds;
|
|
2377
3026
|
}
|
|
2378
3027
|
// -------------------------------------------------------------------------
|
|
2379
3028
|
// Public fetch methods
|
|
@@ -2391,7 +3040,7 @@ var CloudBillingClient = class {
|
|
|
2391
3040
|
const family = this.machineFamily(machineType);
|
|
2392
3041
|
const result = this.extractComputePrice(skus, machineType, family, region);
|
|
2393
3042
|
if (result) {
|
|
2394
|
-
this.cache.set(cacheKey, result, "gcp", "compute", region,
|
|
3043
|
+
this.cache.set(cacheKey, result, "gcp", "compute", region, this.ttlSeconds);
|
|
2395
3044
|
return result;
|
|
2396
3045
|
}
|
|
2397
3046
|
} catch (err) {
|
|
@@ -2414,7 +3063,7 @@ var CloudBillingClient = class {
|
|
|
2414
3063
|
const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_SQL, region);
|
|
2415
3064
|
const result = this.extractDatabasePrice(skus, tier, region);
|
|
2416
3065
|
if (result) {
|
|
2417
|
-
this.cache.set(cacheKey, result, "gcp", "cloud-sql", region,
|
|
3066
|
+
this.cache.set(cacheKey, result, "gcp", "cloud-sql", region, this.ttlSeconds);
|
|
2418
3067
|
return result;
|
|
2419
3068
|
}
|
|
2420
3069
|
} catch (err) {
|
|
@@ -2438,7 +3087,7 @@ var CloudBillingClient = class {
|
|
|
2438
3087
|
const skus = await this.fetchSkus(SERVICE_IDS.CLOUD_STORAGE, region);
|
|
2439
3088
|
const result = this.extractStoragePrice(skus, storageClass, region);
|
|
2440
3089
|
if (result) {
|
|
2441
|
-
this.cache.set(cacheKey, result, "gcp", "cloud-storage", region,
|
|
3090
|
+
this.cache.set(cacheKey, result, "gcp", "cloud-storage", region, this.ttlSeconds);
|
|
2442
3091
|
return result;
|
|
2443
3092
|
}
|
|
2444
3093
|
} catch (err) {
|
|
@@ -2450,6 +3099,141 @@ var CloudBillingClient = class {
|
|
|
2450
3099
|
}
|
|
2451
3100
|
return null;
|
|
2452
3101
|
}
|
|
3102
|
+
/**
|
|
3103
|
+
* Fetch Compute Engine GPU (accelerator) SKUs and return the hourly price
|
|
3104
|
+
* for the given accelerator type (e.g. "nvidia-tesla-t4", "nvidia-a100").
|
|
3105
|
+
*
|
|
3106
|
+
* GPUs live under the Compute service (6F81-5844-456A) with
|
|
3107
|
+
* `category.resourceFamily == "Compute"` and
|
|
3108
|
+
* `category.resourceGroup == "GPU"`. Descriptions follow the pattern
|
|
3109
|
+
* "Nvidia Tesla T4 GPU running in Americas".
|
|
3110
|
+
*/
|
|
3111
|
+
async fetchGpuSkus(accelerator, region) {
|
|
3112
|
+
const cacheKey = `gcp/gpu/${region}/${accelerator.toLowerCase()}`;
|
|
3113
|
+
const cached = this.cache.get(cacheKey);
|
|
3114
|
+
if (cached) return cached;
|
|
3115
|
+
try {
|
|
3116
|
+
const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
|
|
3117
|
+
const result = this.extractGpuPrice(skus, accelerator, region);
|
|
3118
|
+
if (result) {
|
|
3119
|
+
this.cache.set(cacheKey, result, "gcp", "compute-gpu", region, this.ttlSeconds);
|
|
3120
|
+
return result;
|
|
3121
|
+
}
|
|
3122
|
+
} catch (err) {
|
|
3123
|
+
logger.warn("GCP Cloud Billing GPU fetch failed", {
|
|
3124
|
+
accelerator,
|
|
3125
|
+
region,
|
|
3126
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3127
|
+
});
|
|
3128
|
+
}
|
|
3129
|
+
return null;
|
|
3130
|
+
}
|
|
3131
|
+
/**
|
|
3132
|
+
* Fetch Committed Use Discount (CUD) rates for the Compute service in a
|
|
3133
|
+
* region. Returns discount fractions (0..1) against OnDemand base rates,
|
|
3134
|
+
* derived by comparing matching Commit1Yr / Commit3Yr SKUs to their
|
|
3135
|
+
* OnDemand counterparts for the same resource group (e.g. "N1Standard").
|
|
3136
|
+
*
|
|
3137
|
+
* If the API call fails or no matching SKUs are found, returns null and
|
|
3138
|
+
* callers should fall back to static rates.
|
|
3139
|
+
*/
|
|
3140
|
+
async fetchCudRates(region) {
|
|
3141
|
+
const cacheKey = `gcp/cud/${region}`;
|
|
3142
|
+
const cached = this.cache.get(cacheKey);
|
|
3143
|
+
if (cached) return cached;
|
|
3144
|
+
try {
|
|
3145
|
+
const skus = await this.fetchSkus(SERVICE_IDS.COMPUTE, region);
|
|
3146
|
+
const onDemand = /* @__PURE__ */ new Map();
|
|
3147
|
+
const commit1 = /* @__PURE__ */ new Map();
|
|
3148
|
+
const commit3 = /* @__PURE__ */ new Map();
|
|
3149
|
+
const keyOf = (sku) => {
|
|
3150
|
+
const group = sku.category.resourceGroup ?? "";
|
|
3151
|
+
if (!group) return null;
|
|
3152
|
+
const desc = sku.description.toLowerCase();
|
|
3153
|
+
let flavour;
|
|
3154
|
+
if (desc.includes("core") || desc.includes("vcpu")) {
|
|
3155
|
+
flavour = "core";
|
|
3156
|
+
} else if (desc.includes("ram") || desc.includes("memory")) {
|
|
3157
|
+
flavour = "ram";
|
|
3158
|
+
} else {
|
|
3159
|
+
return null;
|
|
3160
|
+
}
|
|
3161
|
+
return `${group}|${flavour}`;
|
|
3162
|
+
};
|
|
3163
|
+
for (const sku of skus) {
|
|
3164
|
+
if (sku.category.resourceFamily !== "Compute") continue;
|
|
3165
|
+
const price = extractUnitPrice(sku);
|
|
3166
|
+
if (price === null) continue;
|
|
3167
|
+
const key = keyOf(sku);
|
|
3168
|
+
if (!key) continue;
|
|
3169
|
+
const usage = sku.category.usageType;
|
|
3170
|
+
if (usage === "OnDemand") {
|
|
3171
|
+
const existing = onDemand.get(key);
|
|
3172
|
+
if (existing === void 0 || price < existing) onDemand.set(key, price);
|
|
3173
|
+
} else if (usage === "Commit1Yr") {
|
|
3174
|
+
const existing = commit1.get(key);
|
|
3175
|
+
if (existing === void 0 || price < existing) commit1.set(key, price);
|
|
3176
|
+
} else if (usage === "Commit3Yr") {
|
|
3177
|
+
const existing = commit3.get(key);
|
|
3178
|
+
if (existing === void 0 || price < existing) commit3.set(key, price);
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
const averageDiscount = (commits) => {
|
|
3182
|
+
const ratios = [];
|
|
3183
|
+
for (const [key, commitPrice] of commits) {
|
|
3184
|
+
const base = onDemand.get(key);
|
|
3185
|
+
if (base === void 0 || base <= 0) continue;
|
|
3186
|
+
const discount = 1 - commitPrice / base;
|
|
3187
|
+
if (discount > 0 && discount < 1) ratios.push(discount);
|
|
3188
|
+
}
|
|
3189
|
+
if (ratios.length === 0) return null;
|
|
3190
|
+
const sum = ratios.reduce((a, b) => a + b, 0);
|
|
3191
|
+
return sum / ratios.length;
|
|
3192
|
+
};
|
|
3193
|
+
const result = {
|
|
3194
|
+
term1yr: averageDiscount(commit1),
|
|
3195
|
+
term3yr: averageDiscount(commit3),
|
|
3196
|
+
sample_count: onDemand.size
|
|
3197
|
+
};
|
|
3198
|
+
if (result.term1yr === null && result.term3yr === null) {
|
|
3199
|
+
return null;
|
|
3200
|
+
}
|
|
3201
|
+
this.cache.set(cacheKey, result, "gcp", "compute-cud", region, this.ttlSeconds);
|
|
3202
|
+
return result;
|
|
3203
|
+
} catch (err) {
|
|
3204
|
+
logger.warn("GCP Cloud Billing CUD fetch failed", {
|
|
3205
|
+
region,
|
|
3206
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3207
|
+
});
|
|
3208
|
+
return null;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
/**
|
|
3212
|
+
* Fetch GKE Autopilot pod resource rates (per-vCPU-hour, per-GB-memory-hour,
|
|
3213
|
+
* per-GB-ephemeral-storage-hour) from the GKE service catalog. Returns a
|
|
3214
|
+
* NormalizedPrice whose `price_per_unit` is the vCPU rate and whose
|
|
3215
|
+
* `attributes` carry the memory/storage rates as strings — callers that
|
|
3216
|
+
* know a pod's resource request can compute an exact per-pod price.
|
|
3217
|
+
*/
|
|
3218
|
+
async fetchAutopilotPodRates(region) {
|
|
3219
|
+
const cacheKey = `gcp/gke-autopilot/${region}`;
|
|
3220
|
+
const cached = this.cache.get(cacheKey);
|
|
3221
|
+
if (cached) return cached;
|
|
3222
|
+
try {
|
|
3223
|
+
const skus = await this.fetchSkus(SERVICE_IDS.GKE, region);
|
|
3224
|
+
const result = this.extractAutopilotPodRates(skus, region);
|
|
3225
|
+
if (result) {
|
|
3226
|
+
this.cache.set(cacheKey, result, "gcp", "gke-autopilot", region, this.ttlSeconds);
|
|
3227
|
+
return result;
|
|
3228
|
+
}
|
|
3229
|
+
} catch (err) {
|
|
3230
|
+
logger.warn("GCP Cloud Billing GKE Autopilot fetch failed", {
|
|
3231
|
+
region,
|
|
3232
|
+
err: err instanceof Error ? err.message : String(err)
|
|
3233
|
+
});
|
|
3234
|
+
}
|
|
3235
|
+
return null;
|
|
3236
|
+
}
|
|
2453
3237
|
// -------------------------------------------------------------------------
|
|
2454
3238
|
// Internal – API fetch
|
|
2455
3239
|
// -------------------------------------------------------------------------
|
|
@@ -2467,10 +3251,14 @@ var CloudBillingClient = class {
|
|
|
2467
3251
|
do {
|
|
2468
3252
|
const pageUrl = pageToken ? `${url}?pageToken=${pageToken}` : url;
|
|
2469
3253
|
logger.debug("GCP Cloud Billing API request", { url: pageUrl, serviceId, region });
|
|
2470
|
-
const res = await
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
3254
|
+
const res = await fetchWithRetryAndCircuitBreaker(
|
|
3255
|
+
pageUrl,
|
|
3256
|
+
{
|
|
3257
|
+
signal: AbortSignal.timeout(3e4),
|
|
3258
|
+
headers: { Accept: "application/json" }
|
|
3259
|
+
},
|
|
3260
|
+
{ maxRetries: 2 }
|
|
3261
|
+
);
|
|
2474
3262
|
if (!res.ok) {
|
|
2475
3263
|
throw new Error(`GCP Billing API HTTP ${res.status} for service ${serviceId}`);
|
|
2476
3264
|
}
|
|
@@ -2599,28 +3387,140 @@ var CloudBillingClient = class {
|
|
|
2599
3387
|
if (price === null) return null;
|
|
2600
3388
|
return {
|
|
2601
3389
|
provider: "gcp",
|
|
2602
|
-
service: "cloud-storage",
|
|
2603
|
-
resource_type: classUpper,
|
|
3390
|
+
service: "cloud-storage",
|
|
3391
|
+
resource_type: classUpper,
|
|
3392
|
+
region,
|
|
3393
|
+
unit: "GiBy.mo",
|
|
3394
|
+
price_per_unit: price,
|
|
3395
|
+
currency: "USD",
|
|
3396
|
+
description: `GCP Cloud Storage ${classUpper} (live: ${sku.description})`,
|
|
3397
|
+
attributes: {
|
|
3398
|
+
storage_class: classUpper,
|
|
3399
|
+
sku_id: sku.skuId,
|
|
3400
|
+
pricing_source: "live"
|
|
3401
|
+
},
|
|
3402
|
+
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3403
|
+
};
|
|
3404
|
+
}
|
|
3405
|
+
/**
|
|
3406
|
+
* Extract a GPU hourly price for the requested accelerator type.
|
|
3407
|
+
* Matches SKUs with resourceGroup == "GPU" whose description contains the
|
|
3408
|
+
* human-readable model name (e.g. "Tesla T4", "A100 40GB").
|
|
3409
|
+
*/
|
|
3410
|
+
extractGpuPrice(skus, accelerator, region) {
|
|
3411
|
+
const lower = accelerator.toLowerCase();
|
|
3412
|
+
const stripped = lower.replace(/^nvidia-?/, "");
|
|
3413
|
+
const tokens = stripped.split(/[-\s]+/).filter((t) => t.length > 0);
|
|
3414
|
+
if (tokens.length === 0) return null;
|
|
3415
|
+
const candidates = skus.filter((s) => {
|
|
3416
|
+
if (s.category.resourceFamily !== "Compute") return false;
|
|
3417
|
+
if (s.category.resourceGroup !== "GPU") return false;
|
|
3418
|
+
if (s.category.usageType !== "OnDemand") return false;
|
|
3419
|
+
const desc = s.description.toLowerCase();
|
|
3420
|
+
return tokens.every((tok) => desc.includes(tok));
|
|
3421
|
+
});
|
|
3422
|
+
if (candidates.length === 0) return null;
|
|
3423
|
+
candidates.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3424
|
+
const sku = candidates[0];
|
|
3425
|
+
const price = extractUnitPrice(sku);
|
|
3426
|
+
if (price === null) return null;
|
|
3427
|
+
return {
|
|
3428
|
+
provider: "gcp",
|
|
3429
|
+
service: "compute-engine",
|
|
3430
|
+
resource_type: accelerator,
|
|
3431
|
+
region,
|
|
3432
|
+
unit: "h",
|
|
3433
|
+
price_per_unit: price,
|
|
3434
|
+
currency: "USD",
|
|
3435
|
+
description: `GCP GPU ${accelerator} (live: ${sku.description})`,
|
|
3436
|
+
attributes: {
|
|
3437
|
+
accelerator,
|
|
3438
|
+
sku_id: sku.skuId,
|
|
3439
|
+
pricing_source: "live"
|
|
3440
|
+
},
|
|
3441
|
+
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3442
|
+
};
|
|
3443
|
+
}
|
|
3444
|
+
/**
|
|
3445
|
+
* Extract GKE Autopilot pod resource rates. Autopilot exposes separate
|
|
3446
|
+
* SKUs per resource dimension (vCPU, memory, ephemeral storage) under the
|
|
3447
|
+
* GKE service. We look for SKUs whose description contains "Autopilot"
|
|
3448
|
+
* and the matching dimension keyword.
|
|
3449
|
+
*/
|
|
3450
|
+
extractAutopilotPodRates(skus, region) {
|
|
3451
|
+
const autopilotSkus = skus.filter((s) => {
|
|
3452
|
+
const desc = s.description.toLowerCase();
|
|
3453
|
+
return s.category.usageType === "OnDemand" && desc.includes("autopilot") && !desc.includes("spot") && !desc.includes("preemptible");
|
|
3454
|
+
});
|
|
3455
|
+
if (autopilotSkus.length === 0) return null;
|
|
3456
|
+
const find = (keywords) => {
|
|
3457
|
+
const matches = autopilotSkus.filter((s) => {
|
|
3458
|
+
const desc = s.description.toLowerCase();
|
|
3459
|
+
return keywords.every((kw) => desc.includes(kw));
|
|
3460
|
+
});
|
|
3461
|
+
if (matches.length === 0) return void 0;
|
|
3462
|
+
matches.sort((a, b) => a.skuId.localeCompare(b.skuId));
|
|
3463
|
+
return matches[0];
|
|
3464
|
+
};
|
|
3465
|
+
const vcpuSku = find(["cpu"]) ?? find(["core"]) ?? find(["vcpu"]);
|
|
3466
|
+
const memorySku = find(["memory"]) ?? find(["ram"]);
|
|
3467
|
+
const storageSku = find(["ephemeral", "storage"]) ?? find(["storage"]);
|
|
3468
|
+
if (!vcpuSku) return null;
|
|
3469
|
+
const vcpuPrice = extractUnitPrice(vcpuSku);
|
|
3470
|
+
if (vcpuPrice === null) return null;
|
|
3471
|
+
const memoryPrice = memorySku ? extractUnitPrice(memorySku) : null;
|
|
3472
|
+
const storagePrice = storageSku ? extractUnitPrice(storageSku) : null;
|
|
3473
|
+
const attributes = {
|
|
3474
|
+
mode: "autopilot",
|
|
3475
|
+
pricing_model: "per_pod_resource",
|
|
3476
|
+
pricing_source: "live",
|
|
3477
|
+
vcpu_hourly: String(vcpuPrice),
|
|
3478
|
+
sku_id_vcpu: vcpuSku.skuId,
|
|
3479
|
+
pricing_note: "Autopilot charges per pod: vCPU, memory, and ephemeral storage are billed separately. price_per_unit reports the vCPU/hr rate; see attributes for memory/storage rates."
|
|
3480
|
+
};
|
|
3481
|
+
if (memoryPrice !== null && memorySku) {
|
|
3482
|
+
attributes.memory_gb_hourly = String(memoryPrice);
|
|
3483
|
+
attributes.sku_id_memory = memorySku.skuId;
|
|
3484
|
+
}
|
|
3485
|
+
if (storagePrice !== null && storageSku) {
|
|
3486
|
+
attributes.ephemeral_storage_gb_hourly = String(storagePrice);
|
|
3487
|
+
attributes.sku_id_storage = storageSku.skuId;
|
|
3488
|
+
}
|
|
3489
|
+
return {
|
|
3490
|
+
provider: "gcp",
|
|
3491
|
+
service: "gke",
|
|
3492
|
+
resource_type: "autopilot-pod",
|
|
2604
3493
|
region,
|
|
2605
|
-
unit: "
|
|
2606
|
-
price_per_unit:
|
|
3494
|
+
unit: "h",
|
|
3495
|
+
price_per_unit: vcpuPrice,
|
|
2607
3496
|
currency: "USD",
|
|
2608
|
-
description: `GCP
|
|
2609
|
-
attributes
|
|
2610
|
-
|
|
2611
|
-
sku_id: sku.skuId,
|
|
2612
|
-
pricing_source: "live"
|
|
2613
|
-
},
|
|
2614
|
-
effective_date: sku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3497
|
+
description: `GCP GKE Autopilot pod resources (live: ${vcpuSku.description})`,
|
|
3498
|
+
attributes,
|
|
3499
|
+
effective_date: vcpuSku.pricingInfo?.[0]?.effectiveTime ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2615
3500
|
};
|
|
2616
3501
|
}
|
|
2617
3502
|
};
|
|
2618
3503
|
|
|
2619
3504
|
// src/pricing/pricing-engine.ts
|
|
3505
|
+
var DEFAULT_CACHE_TTL_SECONDS = 86400;
|
|
2620
3506
|
var AwsProvider = class {
|
|
2621
3507
|
loader;
|
|
2622
|
-
|
|
2623
|
-
|
|
3508
|
+
spotClient;
|
|
3509
|
+
reservedClient;
|
|
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);
|
|
3514
|
+
}
|
|
3515
|
+
getSpotFactor(instanceType, region, os) {
|
|
3516
|
+
return this.spotClient.getSpotFactor(
|
|
3517
|
+
instanceType,
|
|
3518
|
+
region,
|
|
3519
|
+
os === "Windows" ? "Windows" : "Linux"
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
getReservedRates(_instanceType, _region, _os) {
|
|
3523
|
+
return Promise.resolve(null);
|
|
2624
3524
|
}
|
|
2625
3525
|
getComputePrice(instanceType, region, os) {
|
|
2626
3526
|
return this.loader.getComputePrice(instanceType, region, os);
|
|
@@ -2643,8 +3543,8 @@ var AwsProvider = class {
|
|
|
2643
3543
|
};
|
|
2644
3544
|
var AzureProvider = class {
|
|
2645
3545
|
client;
|
|
2646
|
-
constructor(cache) {
|
|
2647
|
-
this.client = new AzureRetailClient(cache);
|
|
3546
|
+
constructor(cache, ttlSeconds) {
|
|
3547
|
+
this.client = new AzureRetailClient(cache, ttlSeconds);
|
|
2648
3548
|
}
|
|
2649
3549
|
getComputePrice(instanceType, region, os) {
|
|
2650
3550
|
return this.client.getComputePrice(instanceType, region, os);
|
|
@@ -2664,13 +3564,31 @@ var AzureProvider = class {
|
|
|
2664
3564
|
getKubernetesPrice(region) {
|
|
2665
3565
|
return this.client.getKubernetesPrice(region);
|
|
2666
3566
|
}
|
|
3567
|
+
/**
|
|
3568
|
+
* Azure-specific: fetch live Spot VM pricing from the Retail API. Exposed
|
|
3569
|
+
* on the provider so the compute calculator can prefer live spot rows
|
|
3570
|
+
* over static discount factors. Returns null when the live API has no
|
|
3571
|
+
* matching spot row. Not part of the base `PricingProvider` interface
|
|
3572
|
+
* because AWS/GCP use different spot channels.
|
|
3573
|
+
*/
|
|
3574
|
+
getSpotPrice(vmSize, region, os) {
|
|
3575
|
+
return this.client.getSpotPrice(vmSize, region, os);
|
|
3576
|
+
}
|
|
3577
|
+
/**
|
|
3578
|
+
* Azure-specific: fetch the live Reservation VM hourly rate (1yr / 3yr)
|
|
3579
|
+
* from the Retail API. Returns null when no reservation row matches,
|
|
3580
|
+
* signalling callers to fall back to the static DISCOUNT_RATES table.
|
|
3581
|
+
*/
|
|
3582
|
+
getReservationHourlyRate(vmSize, region, term) {
|
|
3583
|
+
return this.client.getReservationHourlyRate(vmSize, region, term);
|
|
3584
|
+
}
|
|
2667
3585
|
};
|
|
2668
3586
|
var GcpProvider = class {
|
|
2669
3587
|
loader;
|
|
2670
3588
|
liveClient;
|
|
2671
|
-
constructor(cache) {
|
|
3589
|
+
constructor(cache, ttlSeconds) {
|
|
2672
3590
|
this.loader = new GcpBundledLoader();
|
|
2673
|
-
this.liveClient = new CloudBillingClient(cache);
|
|
3591
|
+
this.liveClient = new CloudBillingClient(cache, ttlSeconds);
|
|
2674
3592
|
}
|
|
2675
3593
|
async getComputePrice(instanceType, region, _os) {
|
|
2676
3594
|
try {
|
|
@@ -2724,12 +3642,51 @@ var GcpProvider = class {
|
|
|
2724
3642
|
return this.loader.getKubernetesPrice(region, mode === "autopilot" ? "autopilot" : "standard");
|
|
2725
3643
|
}
|
|
2726
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
|
+
})();
|
|
2727
3681
|
var PricingEngine = class {
|
|
2728
3682
|
providers = /* @__PURE__ */ new Map();
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
this.
|
|
2732
|
-
|
|
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));
|
|
2733
3690
|
}
|
|
2734
3691
|
/**
|
|
2735
3692
|
* Return the PricingProvider for a specific cloud provider.
|
|
@@ -2764,24 +3721,9 @@ var PricingEngine = class {
|
|
|
2764
3721
|
resourceType,
|
|
2765
3722
|
region
|
|
2766
3723
|
});
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
if (svc === "database" || svc === "rds" || svc === "sql" || svc === "db" || svc === "cloud-sql" || svc === "azure-database") {
|
|
2771
|
-
return p.getDatabasePrice(resourceType, region, attributes.engine);
|
|
2772
|
-
}
|
|
2773
|
-
if (svc === "storage" || svc === "ebs" || svc === "disk" || svc === "gcs" || svc === "managed-disks" || svc === "persistent-disk" || svc === "cloud-storage") {
|
|
2774
|
-
const sizeGb = attributes.size_gb ? parseFloat(attributes.size_gb) : void 0;
|
|
2775
|
-
return p.getStoragePrice(resourceType, region, sizeGb);
|
|
2776
|
-
}
|
|
2777
|
-
if (svc === "lb" || svc === "load-balancer" || svc === "alb" || svc === "nlb" || svc === "elb" || svc === "cloud-load-balancing") {
|
|
2778
|
-
return p.getLoadBalancerPrice(resourceType, region);
|
|
2779
|
-
}
|
|
2780
|
-
if (svc === "nat" || svc === "nat-gateway" || svc === "cloud-nat") {
|
|
2781
|
-
return p.getNatGatewayPrice(region);
|
|
2782
|
-
}
|
|
2783
|
-
if (svc === "k8s" || svc === "kubernetes" || svc === "eks" || svc === "aks" || svc === "gke") {
|
|
2784
|
-
return p.getKubernetesPrice(region, attributes.mode);
|
|
3724
|
+
const lookup = SERVICE_ALIAS_LOOKUPS.get(svc);
|
|
3725
|
+
if (lookup) {
|
|
3726
|
+
return lookup(p, resourceType, region, attributes);
|
|
2785
3727
|
}
|
|
2786
3728
|
logger.warn("PricingEngine: unrecognised service", { service, provider });
|
|
2787
3729
|
return null;
|
|
@@ -2789,13 +3731,27 @@ var PricingEngine = class {
|
|
|
2789
3731
|
};
|
|
2790
3732
|
|
|
2791
3733
|
// src/tools/analyze-terraform.ts
|
|
2792
|
-
import { z } from "zod";
|
|
3734
|
+
import { z as z3 } from "zod";
|
|
2793
3735
|
|
|
2794
3736
|
// src/parsers/index.ts
|
|
2795
|
-
import { dirname as dirname2 } from "path";
|
|
3737
|
+
import { dirname as dirname2, isAbsolute, normalize as normalize4, sep as sep2 } from "path";
|
|
2796
3738
|
|
|
2797
3739
|
// src/parsers/hcl-parser.ts
|
|
2798
3740
|
import { parse } from "@cdktf/hcl2json";
|
|
3741
|
+
|
|
3742
|
+
// src/util/sanitize.ts
|
|
3743
|
+
function sanitizeForMessage(input, maxLen = 256) {
|
|
3744
|
+
const s = typeof input === "string" ? input : String(input);
|
|
3745
|
+
const noControl = s.replace(/[\x00-\x1F\x7F]/g, "");
|
|
3746
|
+
const noInvisible = noControl.replace(
|
|
3747
|
+
/[\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/g,
|
|
3748
|
+
""
|
|
3749
|
+
);
|
|
3750
|
+
if (noInvisible.length <= maxLen) return noInvisible;
|
|
3751
|
+
return `${noInvisible.slice(0, maxLen)}\u2026`;
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// src/parsers/hcl-parser.ts
|
|
2799
3755
|
async function parseHclToJson(hclContent, filename = "main.tf") {
|
|
2800
3756
|
if (!hclContent.trim()) {
|
|
2801
3757
|
logger.debug("Received empty HCL content, returning empty object", {
|
|
@@ -2809,9 +3765,44 @@ async function parseHclToJson(hclContent, filename = "main.tf") {
|
|
|
2809
3765
|
return result;
|
|
2810
3766
|
} catch (err) {
|
|
2811
3767
|
const message = err instanceof Error ? err.message : String(err);
|
|
2812
|
-
|
|
2813
|
-
|
|
3768
|
+
const safeFilename = sanitizeForMessage(filename, 256);
|
|
3769
|
+
const safeMessage = sanitizeForMessage(message, 512);
|
|
3770
|
+
logger.error("Failed to parse HCL", { filename: safeFilename, error: safeMessage });
|
|
3771
|
+
throw new Error(`HCL parse error in "${safeFilename}": ${safeMessage}`);
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
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;
|
|
2814
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;
|
|
2815
3806
|
}
|
|
2816
3807
|
|
|
2817
3808
|
// src/parsers/variable-resolver.ts
|
|
@@ -2892,7 +3883,8 @@ function substituteInString(str2, variables) {
|
|
|
2892
3883
|
return str2.replace(VAR_INLINE_RE, (_match, varName) => {
|
|
2893
3884
|
if (varName in variables) {
|
|
2894
3885
|
const v = variables[varName];
|
|
2895
|
-
|
|
3886
|
+
if (v === null || v === void 0) return "";
|
|
3887
|
+
return coerceToString(v);
|
|
2896
3888
|
}
|
|
2897
3889
|
return _match;
|
|
2898
3890
|
});
|
|
@@ -2915,32 +3907,6 @@ function detectProvider(resourceType) {
|
|
|
2915
3907
|
);
|
|
2916
3908
|
}
|
|
2917
3909
|
|
|
2918
|
-
// src/parsers/extractors/helpers.ts
|
|
2919
|
-
function firstBlock(block, key) {
|
|
2920
|
-
const nested = block[key];
|
|
2921
|
-
if (Array.isArray(nested) && nested.length > 0) {
|
|
2922
|
-
return nested[0];
|
|
2923
|
-
}
|
|
2924
|
-
return {};
|
|
2925
|
-
}
|
|
2926
|
-
function str(v) {
|
|
2927
|
-
return typeof v === "string" && v ? v : void 0;
|
|
2928
|
-
}
|
|
2929
|
-
function num(v) {
|
|
2930
|
-
if (typeof v === "number") return v;
|
|
2931
|
-
if (typeof v === "string") {
|
|
2932
|
-
const n = Number(v);
|
|
2933
|
-
return isNaN(n) ? void 0 : n;
|
|
2934
|
-
}
|
|
2935
|
-
return void 0;
|
|
2936
|
-
}
|
|
2937
|
-
function bool(v) {
|
|
2938
|
-
if (typeof v === "boolean") return v;
|
|
2939
|
-
if (v === "true") return true;
|
|
2940
|
-
if (v === "false") return false;
|
|
2941
|
-
return void 0;
|
|
2942
|
-
}
|
|
2943
|
-
|
|
2944
3910
|
// src/parsers/extractors/aws-extractor.ts
|
|
2945
3911
|
var awsExtractors = {
|
|
2946
3912
|
aws_instance(block) {
|
|
@@ -3526,7 +4492,7 @@ function extractTags(block) {
|
|
|
3526
4492
|
const result = {};
|
|
3527
4493
|
for (const [k, v] of Object.entries(tags)) {
|
|
3528
4494
|
if (typeof v === "string") result[k] = v;
|
|
3529
|
-
else if (v !== null && v !== void 0) result[k] =
|
|
4495
|
+
else if (v !== null && v !== void 0) result[k] = coerceToString(v);
|
|
3530
4496
|
}
|
|
3531
4497
|
return result;
|
|
3532
4498
|
}
|
|
@@ -3638,14 +4604,63 @@ function extractResources(hclJson, variables, sourceFile, defaultRegions = {}, w
|
|
|
3638
4604
|
|
|
3639
4605
|
// src/parsers/module-resolver.ts
|
|
3640
4606
|
import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
3641
|
-
import { join as join2, resolve } from "path";
|
|
4607
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
4608
|
+
|
|
4609
|
+
// src/parsers/path-safety.ts
|
|
4610
|
+
import { lstatSync, realpathSync } from "fs";
|
|
4611
|
+
import { resolve, sep } from "path";
|
|
4612
|
+
function resolveWithinBoundary(candidate, basePath, rootBoundary) {
|
|
4613
|
+
const resolved = resolve(basePath, candidate);
|
|
4614
|
+
const boundary = resolve(rootBoundary);
|
|
4615
|
+
if (!isWithin(resolved, boundary)) return null;
|
|
4616
|
+
try {
|
|
4617
|
+
const real = realpathSync(resolved);
|
|
4618
|
+
if (!isWithin(real, boundary)) return null;
|
|
4619
|
+
const stat = lstatSync(resolved);
|
|
4620
|
+
if (stat.isSymbolicLink()) return null;
|
|
4621
|
+
return real;
|
|
4622
|
+
} catch {
|
|
4623
|
+
return resolved;
|
|
4624
|
+
}
|
|
4625
|
+
}
|
|
4626
|
+
function isWithin(child, parent) {
|
|
4627
|
+
if (child === parent) return true;
|
|
4628
|
+
const parentWithSep = parent.endsWith(sep) ? parent : parent + sep;
|
|
4629
|
+
return child.startsWith(parentWithSep);
|
|
4630
|
+
}
|
|
4631
|
+
|
|
4632
|
+
// src/parsers/safe-json.ts
|
|
4633
|
+
var FORBIDDEN_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
4634
|
+
function safeJsonParse(text) {
|
|
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
|
+
}
|
|
4641
|
+
return value;
|
|
4642
|
+
});
|
|
4643
|
+
if (droppedKeys > 0) {
|
|
4644
|
+
logger.warn("safeJsonParse: dropped prototype-pollution keys from payload", {
|
|
4645
|
+
droppedKeys
|
|
4646
|
+
});
|
|
4647
|
+
}
|
|
4648
|
+
return parsed;
|
|
4649
|
+
}
|
|
4650
|
+
|
|
4651
|
+
// src/parsers/module-resolver.ts
|
|
3642
4652
|
var MAX_MODULE_DEPTH = 10;
|
|
4653
|
+
var FORBIDDEN_MERGE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
3643
4654
|
function readTfFiles(dirPath) {
|
|
3644
4655
|
if (!existsSync2(dirPath)) return [];
|
|
3645
4656
|
let entries;
|
|
3646
4657
|
try {
|
|
3647
4658
|
entries = readdirSync(dirPath);
|
|
3648
|
-
} 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
|
+
});
|
|
3649
4664
|
return [];
|
|
3650
4665
|
}
|
|
3651
4666
|
const files = [];
|
|
@@ -3655,7 +4670,11 @@ function readTfFiles(dirPath) {
|
|
|
3655
4670
|
try {
|
|
3656
4671
|
const content = readFileSync2(fullPath, "utf-8");
|
|
3657
4672
|
files.push({ path: fullPath, content });
|
|
3658
|
-
} 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
|
+
});
|
|
3659
4678
|
}
|
|
3660
4679
|
}
|
|
3661
4680
|
return files;
|
|
@@ -3664,6 +4683,7 @@ function mergeHclJsons(jsons) {
|
|
|
3664
4683
|
const merged = {};
|
|
3665
4684
|
for (const json of jsons) {
|
|
3666
4685
|
for (const [key, value] of Object.entries(json)) {
|
|
4686
|
+
if (FORBIDDEN_MERGE_KEYS.has(key)) continue;
|
|
3667
4687
|
if (!(key in merged)) {
|
|
3668
4688
|
merged[key] = JSON.parse(JSON.stringify(value));
|
|
3669
4689
|
continue;
|
|
@@ -3684,8 +4704,13 @@ function mergeHclJsons(jsons) {
|
|
|
3684
4704
|
return merged;
|
|
3685
4705
|
}
|
|
3686
4706
|
function mergeObjects(a, b) {
|
|
3687
|
-
const result = {
|
|
4707
|
+
const result = {};
|
|
4708
|
+
for (const [key, aVal] of Object.entries(a)) {
|
|
4709
|
+
if (FORBIDDEN_MERGE_KEYS.has(key)) continue;
|
|
4710
|
+
result[key] = aVal;
|
|
4711
|
+
}
|
|
3688
4712
|
for (const [key, bVal] of Object.entries(b)) {
|
|
4713
|
+
if (FORBIDDEN_MERGE_KEYS.has(key)) continue;
|
|
3689
4714
|
const aVal = result[key];
|
|
3690
4715
|
if (aVal !== null && typeof aVal === "object" && !Array.isArray(aVal) && bVal !== null && typeof bVal === "object" && !Array.isArray(bVal)) {
|
|
3691
4716
|
result[key] = mergeObjects(aVal, bVal);
|
|
@@ -3727,10 +4752,10 @@ function prefixResources(resources, moduleName) {
|
|
|
3727
4752
|
id: `module.${moduleName}.${r.id}`
|
|
3728
4753
|
}));
|
|
3729
4754
|
}
|
|
3730
|
-
async function parseModuleDirectory(dirPath, parentVars, moduleInputs, warnings, depth) {
|
|
4755
|
+
async function parseModuleDirectory(dirPath, parentVars, moduleInputs, warnings, depth, rootBoundary) {
|
|
3731
4756
|
if (depth >= MAX_MODULE_DEPTH) {
|
|
3732
4757
|
warnings.push(
|
|
3733
|
-
`Module nesting depth limit (${MAX_MODULE_DEPTH}) reached at "${dirPath}". Skipping further expansion.`
|
|
4758
|
+
`Module nesting depth limit (${MAX_MODULE_DEPTH}) reached at "${sanitizeForMessage(dirPath)}". Skipping further expansion.`
|
|
3734
4759
|
);
|
|
3735
4760
|
return [];
|
|
3736
4761
|
}
|
|
@@ -3745,10 +4770,12 @@ async function parseModuleDirectory(dirPath, parentVars, moduleInputs, warnings,
|
|
|
3745
4770
|
parsedJsons.push({ path: file.path, json });
|
|
3746
4771
|
} catch (err) {
|
|
3747
4772
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3748
|
-
|
|
4773
|
+
const safePath = sanitizeForMessage(file.path);
|
|
4774
|
+
const safeMsg = sanitizeForMessage(msg, 512);
|
|
4775
|
+
warnings.push(`Parse error in module file ${safePath}: ${safeMsg}`);
|
|
3749
4776
|
logger.warn("Skipping module file due to parse error", {
|
|
3750
|
-
path:
|
|
3751
|
-
error:
|
|
4777
|
+
path: safePath,
|
|
4778
|
+
error: safeMsg
|
|
3752
4779
|
});
|
|
3753
4780
|
}
|
|
3754
4781
|
}
|
|
@@ -3771,86 +4798,117 @@ async function parseModuleDirectory(dirPath, parentVars, moduleInputs, warnings,
|
|
|
3771
4798
|
allResources.push(...resources);
|
|
3772
4799
|
} catch (err) {
|
|
3773
4800
|
const msg = err instanceof Error ? err.message : String(err);
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
4801
|
+
const safePath = sanitizeForMessage(path);
|
|
4802
|
+
const safeMsg = sanitizeForMessage(msg, 512);
|
|
4803
|
+
warnings.push(`Extraction error in module file ${safePath}: ${safeMsg}`);
|
|
4804
|
+
logger.warn("Resource extraction failed in module", { path: safePath, error: safeMsg });
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
const nestedResources = await resolveModules(
|
|
4808
|
+
combined,
|
|
4809
|
+
dirPath,
|
|
4810
|
+
variables,
|
|
4811
|
+
warnings,
|
|
4812
|
+
depth + 1,
|
|
4813
|
+
rootBoundary
|
|
4814
|
+
);
|
|
3779
4815
|
allResources.push(...nestedResources);
|
|
3780
4816
|
return allResources;
|
|
3781
4817
|
}
|
|
3782
|
-
async function resolveModules(hclJson, basePath, variables, warnings, depth = 0) {
|
|
4818
|
+
async function resolveModules(hclJson, basePath, variables, warnings, depth = 0, rootBoundary) {
|
|
3783
4819
|
const moduleBlock = hclJson["module"];
|
|
3784
4820
|
if (!moduleBlock || typeof moduleBlock !== "object" || Array.isArray(moduleBlock)) {
|
|
3785
4821
|
return [];
|
|
3786
4822
|
}
|
|
4823
|
+
const boundary = resolve2(rootBoundary ?? process.cwd());
|
|
3787
4824
|
const allResources = [];
|
|
3788
4825
|
for (const [moduleName, moduleDeclaration] of Object.entries(
|
|
3789
4826
|
moduleBlock
|
|
3790
4827
|
)) {
|
|
4828
|
+
const safeModuleName = sanitizeForMessage(moduleName, 128);
|
|
3791
4829
|
const source = getSourceAttr(moduleDeclaration);
|
|
3792
4830
|
if (!source) {
|
|
3793
|
-
warnings.push(`Module "${
|
|
4831
|
+
warnings.push(`Module "${safeModuleName}" has no source attribute and will be skipped.`);
|
|
3794
4832
|
continue;
|
|
3795
4833
|
}
|
|
4834
|
+
const safeSource = sanitizeForMessage(source, 256);
|
|
3796
4835
|
const isLocal = source.startsWith("./") || source.startsWith("../");
|
|
3797
4836
|
const isGit = source.startsWith("git::") || source.includes("github.com") || source.includes("bitbucket.org") || source.startsWith("hg::");
|
|
3798
4837
|
if (isGit) {
|
|
3799
4838
|
warnings.push(
|
|
3800
|
-
`Module "${
|
|
4839
|
+
`Module "${safeModuleName}" uses a git source ("${safeSource}") which is not supported. Skipping.`
|
|
3801
4840
|
);
|
|
3802
|
-
logger.debug("Skipping git module", { moduleName, source });
|
|
4841
|
+
logger.debug("Skipping git module", { moduleName: safeModuleName, source: safeSource });
|
|
3803
4842
|
continue;
|
|
3804
4843
|
}
|
|
3805
4844
|
const rawInputs = extractModuleInputs(moduleDeclaration);
|
|
3806
4845
|
const moduleInputs = substituteVariables(rawInputs, variables);
|
|
3807
4846
|
let moduleDir;
|
|
3808
4847
|
if (isLocal) {
|
|
3809
|
-
moduleDir =
|
|
4848
|
+
moduleDir = resolveWithinBoundary(source, basePath, boundary);
|
|
4849
|
+
if (!moduleDir) {
|
|
4850
|
+
warnings.push(
|
|
4851
|
+
`Module "${safeModuleName}" source "${safeSource}" resolves outside the allowed root and was rejected.`
|
|
4852
|
+
);
|
|
4853
|
+
logger.warn("Rejected local module path escaping boundary", {
|
|
4854
|
+
moduleName: safeModuleName,
|
|
4855
|
+
source: safeSource
|
|
4856
|
+
});
|
|
4857
|
+
continue;
|
|
4858
|
+
}
|
|
3810
4859
|
} else {
|
|
3811
|
-
const terraformModulesDir =
|
|
3812
|
-
const candidateDirect =
|
|
3813
|
-
|
|
4860
|
+
const terraformModulesDir = resolve2(basePath, ".terraform", "modules");
|
|
4861
|
+
const candidateDirect = resolveWithinBoundary(
|
|
4862
|
+
moduleName,
|
|
4863
|
+
terraformModulesDir,
|
|
4864
|
+
terraformModulesDir
|
|
4865
|
+
);
|
|
4866
|
+
if (candidateDirect && existsSync2(candidateDirect)) {
|
|
3814
4867
|
moduleDir = candidateDirect;
|
|
3815
4868
|
} else {
|
|
3816
4869
|
const modulesJsonPath = join2(terraformModulesDir, "modules.json");
|
|
4870
|
+
let resolvedFromJson = null;
|
|
3817
4871
|
if (existsSync2(modulesJsonPath)) {
|
|
3818
4872
|
try {
|
|
3819
|
-
const
|
|
4873
|
+
const rawJson = readFileSync2(modulesJsonPath, "utf-8");
|
|
4874
|
+
const modulesJson = safeJsonParse(rawJson);
|
|
3820
4875
|
const entry = modulesJson.Modules?.find(
|
|
3821
4876
|
(m) => m.Key === moduleName || m.Key.startsWith(`${moduleName}.`)
|
|
3822
4877
|
);
|
|
3823
|
-
if (entry?.Dir) {
|
|
3824
|
-
|
|
3825
|
-
} else {
|
|
3826
|
-
warnings.push(
|
|
3827
|
-
`Registry module "${moduleName}" (source: "${source}") not found in .terraform/modules/. Run "terraform init" to download modules before estimating costs.`
|
|
3828
|
-
);
|
|
3829
|
-
logger.debug("Registry module not initialised", { moduleName, source });
|
|
3830
|
-
continue;
|
|
4878
|
+
if (entry?.Dir && typeof entry.Dir === "string") {
|
|
4879
|
+
resolvedFromJson = resolveWithinBoundary(entry.Dir, basePath, boundary);
|
|
3831
4880
|
}
|
|
3832
4881
|
} catch {
|
|
3833
|
-
|
|
3834
|
-
`Registry module "${moduleName}" (source: "${source}") not found in .terraform/modules/. Run "terraform init" to download modules before estimating costs.`
|
|
3835
|
-
);
|
|
3836
|
-
continue;
|
|
4882
|
+
resolvedFromJson = null;
|
|
3837
4883
|
}
|
|
4884
|
+
}
|
|
4885
|
+
if (resolvedFromJson) {
|
|
4886
|
+
moduleDir = resolvedFromJson;
|
|
3838
4887
|
} else {
|
|
3839
4888
|
warnings.push(
|
|
3840
|
-
`Registry module "${
|
|
4889
|
+
`Registry module "${safeModuleName}" (source: "${safeSource}") not found in .terraform/modules/. Run "terraform init" to download modules before estimating costs.`
|
|
3841
4890
|
);
|
|
3842
|
-
logger.debug("Registry module not initialised", {
|
|
4891
|
+
logger.debug("Registry module not initialised", {
|
|
4892
|
+
moduleName: safeModuleName,
|
|
4893
|
+
source: safeSource
|
|
4894
|
+
});
|
|
3843
4895
|
continue;
|
|
3844
4896
|
}
|
|
3845
4897
|
}
|
|
3846
4898
|
}
|
|
3847
|
-
logger.debug("Resolving module", {
|
|
4899
|
+
logger.debug("Resolving module", {
|
|
4900
|
+
moduleName: safeModuleName,
|
|
4901
|
+
source: safeSource,
|
|
4902
|
+
moduleDir,
|
|
4903
|
+
depth
|
|
4904
|
+
});
|
|
3848
4905
|
const childResources = await parseModuleDirectory(
|
|
3849
4906
|
moduleDir,
|
|
3850
4907
|
variables,
|
|
3851
4908
|
moduleInputs,
|
|
3852
4909
|
warnings,
|
|
3853
|
-
depth
|
|
4910
|
+
depth,
|
|
4911
|
+
boundary
|
|
3854
4912
|
);
|
|
3855
4913
|
allResources.push(...prefixResources(childResources, moduleName));
|
|
3856
4914
|
}
|
|
@@ -3927,7 +4985,7 @@ function extractAttributes(cfnType, props) {
|
|
|
3927
4985
|
return {
|
|
3928
4986
|
instance_type: asString(props.DBInstanceClass),
|
|
3929
4987
|
engine: asString(props.Engine),
|
|
3930
|
-
storage_size_gb:
|
|
4988
|
+
storage_size_gb: num(props.AllocatedStorage),
|
|
3931
4989
|
multi_az: asBoolean(props.MultiAZ)
|
|
3932
4990
|
};
|
|
3933
4991
|
case "AWS::S3::Bucket":
|
|
@@ -3935,8 +4993,8 @@ function extractAttributes(cfnType, props) {
|
|
|
3935
4993
|
case "AWS::EBS::Volume":
|
|
3936
4994
|
return {
|
|
3937
4995
|
storage_type: asString(props.VolumeType),
|
|
3938
|
-
storage_size_gb:
|
|
3939
|
-
iops:
|
|
4996
|
+
storage_size_gb: num(props.Size),
|
|
4997
|
+
iops: num(props.Iops)
|
|
3940
4998
|
};
|
|
3941
4999
|
case "AWS::ElasticLoadBalancingV2::LoadBalancer":
|
|
3942
5000
|
return {
|
|
@@ -3948,16 +5006,16 @@ function extractAttributes(cfnType, props) {
|
|
|
3948
5006
|
return {};
|
|
3949
5007
|
case "AWS::Lambda::Function":
|
|
3950
5008
|
return {
|
|
3951
|
-
memory_size:
|
|
3952
|
-
timeout:
|
|
5009
|
+
memory_size: num(props.MemorySize),
|
|
5010
|
+
timeout: num(props.Timeout)
|
|
3953
5011
|
};
|
|
3954
5012
|
case "AWS::DynamoDB::Table":
|
|
3955
5013
|
return {
|
|
3956
5014
|
billing_mode: asString(props.BillingMode) ?? "PROVISIONED",
|
|
3957
|
-
read_capacity:
|
|
5015
|
+
read_capacity: num(
|
|
3958
5016
|
props.ProvisionedThroughput?.ReadCapacityUnits
|
|
3959
5017
|
),
|
|
3960
|
-
write_capacity:
|
|
5018
|
+
write_capacity: num(
|
|
3961
5019
|
props.ProvisionedThroughput?.WriteCapacityUnits
|
|
3962
5020
|
)
|
|
3963
5021
|
};
|
|
@@ -3972,7 +5030,7 @@ function extractAttributes(cfnType, props) {
|
|
|
3972
5030
|
case "AWS::ElastiCache::CacheCluster":
|
|
3973
5031
|
return {
|
|
3974
5032
|
instance_type: asString(props.CacheNodeType),
|
|
3975
|
-
node_count:
|
|
5033
|
+
node_count: num(props.NumCacheNodes),
|
|
3976
5034
|
engine: asString(props.Engine)
|
|
3977
5035
|
};
|
|
3978
5036
|
case "AWS::CloudFront::Distribution":
|
|
@@ -3987,7 +5045,8 @@ function extractTags2(props) {
|
|
|
3987
5045
|
if (Array.isArray(rawTags)) {
|
|
3988
5046
|
for (const tag of rawTags) {
|
|
3989
5047
|
if (tag && typeof tag === "object" && "Key" in tag && "Value" in tag) {
|
|
3990
|
-
|
|
5048
|
+
const { Key, Value } = tag;
|
|
5049
|
+
tags[coerceToString(Key)] = coerceToString(Value);
|
|
3991
5050
|
}
|
|
3992
5051
|
}
|
|
3993
5052
|
}
|
|
@@ -3996,14 +5055,6 @@ function extractTags2(props) {
|
|
|
3996
5055
|
function asString(value) {
|
|
3997
5056
|
return typeof value === "string" ? value : void 0;
|
|
3998
5057
|
}
|
|
3999
|
-
function asNumber(value) {
|
|
4000
|
-
if (typeof value === "number") return value;
|
|
4001
|
-
if (typeof value === "string") {
|
|
4002
|
-
const n = Number(value);
|
|
4003
|
-
return Number.isNaN(n) ? void 0 : n;
|
|
4004
|
-
}
|
|
4005
|
-
return void 0;
|
|
4006
|
-
}
|
|
4007
5058
|
function asBoolean(value) {
|
|
4008
5059
|
if (typeof value === "boolean") return value;
|
|
4009
5060
|
if (value === "true") return true;
|
|
@@ -4023,11 +5074,11 @@ var CloudFormationParser = class {
|
|
|
4023
5074
|
return false;
|
|
4024
5075
|
});
|
|
4025
5076
|
}
|
|
4026
|
-
|
|
5077
|
+
parse(files, options) {
|
|
4027
5078
|
const warnings = [];
|
|
4028
5079
|
const file = files.find((f) => this.extensions.some((ext) => f.path.endsWith(ext)));
|
|
4029
5080
|
if (!file) {
|
|
4030
|
-
return emptyInventory(warnings);
|
|
5081
|
+
return Promise.resolve(emptyInventory(warnings));
|
|
4031
5082
|
}
|
|
4032
5083
|
let template;
|
|
4033
5084
|
try {
|
|
@@ -4036,11 +5087,11 @@ var CloudFormationParser = class {
|
|
|
4036
5087
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4037
5088
|
warnings.push(`Failed to parse CloudFormation template ${file.path}: ${msg}`);
|
|
4038
5089
|
logger.warn("CloudFormation parse error", { path: file.path, error: msg });
|
|
4039
|
-
return emptyInventory(warnings);
|
|
5090
|
+
return Promise.resolve(emptyInventory(warnings));
|
|
4040
5091
|
}
|
|
4041
5092
|
if (!template || typeof template !== "object" || !template.Resources) {
|
|
4042
5093
|
warnings.push(`Invalid CloudFormation template: missing Resources section in ${file.path}`);
|
|
4043
|
-
return emptyInventory(warnings);
|
|
5094
|
+
return Promise.resolve(emptyInventory(warnings));
|
|
4044
5095
|
}
|
|
4045
5096
|
const resolvedParams = resolveParameters(
|
|
4046
5097
|
template.Parameters ?? {},
|
|
@@ -4073,7 +5124,7 @@ var CloudFormationParser = class {
|
|
|
4073
5124
|
region,
|
|
4074
5125
|
warningCount: warnings.length
|
|
4075
5126
|
});
|
|
4076
|
-
return inventory;
|
|
5127
|
+
return Promise.resolve(inventory);
|
|
4077
5128
|
}
|
|
4078
5129
|
};
|
|
4079
5130
|
function emptyInventory(warnings) {
|
|
@@ -4179,14 +5230,14 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
|
|
|
4179
5230
|
const diskProps = props;
|
|
4180
5231
|
return {
|
|
4181
5232
|
sku: resource.sku?.name,
|
|
4182
|
-
storage_size_gb:
|
|
5233
|
+
storage_size_gb: num(diskProps.diskSizeGB)
|
|
4183
5234
|
};
|
|
4184
5235
|
}
|
|
4185
5236
|
case "microsoft.dbforpostgresql/flexibleservers": {
|
|
4186
5237
|
const storage = props.storage;
|
|
4187
5238
|
return {
|
|
4188
5239
|
sku: resource.sku?.name,
|
|
4189
|
-
storage_size_gb:
|
|
5240
|
+
storage_size_gb: num(storage?.storageSizeGB)
|
|
4190
5241
|
};
|
|
4191
5242
|
}
|
|
4192
5243
|
case "microsoft.dbformysql/flexibleservers":
|
|
@@ -4210,7 +5261,7 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
|
|
|
4210
5261
|
const pools = props.agentPoolProfiles;
|
|
4211
5262
|
const firstPool = pools?.[0];
|
|
4212
5263
|
return {
|
|
4213
|
-
node_count:
|
|
5264
|
+
node_count: num(firstPool?.count),
|
|
4214
5265
|
vm_size: typeof firstPool?.vmSize === "string" ? firstPool.vmSize : void 0
|
|
4215
5266
|
};
|
|
4216
5267
|
}
|
|
@@ -4244,14 +5295,6 @@ function extractAttributes2(armTypeLower, props, resource, parameters) {
|
|
|
4244
5295
|
return {};
|
|
4245
5296
|
}
|
|
4246
5297
|
}
|
|
4247
|
-
function asNumber2(value) {
|
|
4248
|
-
if (typeof value === "number") return value;
|
|
4249
|
-
if (typeof value === "string") {
|
|
4250
|
-
const n = Number(value);
|
|
4251
|
-
return Number.isNaN(n) ? void 0 : n;
|
|
4252
|
-
}
|
|
4253
|
-
return void 0;
|
|
4254
|
-
}
|
|
4255
5298
|
|
|
4256
5299
|
// src/parsers/bicep/arm-parser.ts
|
|
4257
5300
|
var ArmParser = class {
|
|
@@ -4263,11 +5306,11 @@ var ArmParser = class {
|
|
|
4263
5306
|
return f.content.includes("schema.management.azure.com") || f.content.includes("Microsoft.") && f.content.includes("apiVersion");
|
|
4264
5307
|
});
|
|
4265
5308
|
}
|
|
4266
|
-
|
|
5309
|
+
parse(files, options) {
|
|
4267
5310
|
const warnings = [];
|
|
4268
5311
|
const file = files.find((f) => f.path.endsWith(".json"));
|
|
4269
5312
|
if (!file) {
|
|
4270
|
-
return emptyInventory2(warnings);
|
|
5313
|
+
return Promise.resolve(emptyInventory2(warnings));
|
|
4271
5314
|
}
|
|
4272
5315
|
let template;
|
|
4273
5316
|
try {
|
|
@@ -4276,11 +5319,11 @@ var ArmParser = class {
|
|
|
4276
5319
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4277
5320
|
warnings.push(`Failed to parse ARM template ${file.path}: ${msg}`);
|
|
4278
5321
|
logger.warn("ARM template parse error", { path: file.path, error: msg });
|
|
4279
|
-
return emptyInventory2(warnings);
|
|
5322
|
+
return Promise.resolve(emptyInventory2(warnings));
|
|
4280
5323
|
}
|
|
4281
5324
|
if (!template || typeof template !== "object" || !Array.isArray(template.resources)) {
|
|
4282
5325
|
warnings.push(`Invalid ARM template: missing resources array in ${file.path}`);
|
|
4283
|
-
return emptyInventory2(warnings);
|
|
5326
|
+
return Promise.resolve(emptyInventory2(warnings));
|
|
4284
5327
|
}
|
|
4285
5328
|
const resolvedParams = resolveParameters2(
|
|
4286
5329
|
template.parameters ?? {},
|
|
@@ -4313,7 +5356,7 @@ var ArmParser = class {
|
|
|
4313
5356
|
region,
|
|
4314
5357
|
warningCount: warnings.length
|
|
4315
5358
|
});
|
|
4316
|
-
return inventory;
|
|
5359
|
+
return Promise.resolve(inventory);
|
|
4317
5360
|
}
|
|
4318
5361
|
};
|
|
4319
5362
|
function emptyInventory2(warnings) {
|
|
@@ -4463,7 +5506,7 @@ function extractAttributes3(pulumiType, props) {
|
|
|
4463
5506
|
return {
|
|
4464
5507
|
instance_type: asString2(props.instanceClass),
|
|
4465
5508
|
engine: asString2(props.engine),
|
|
4466
|
-
storage_size_gb:
|
|
5509
|
+
storage_size_gb: num(props.allocatedStorage),
|
|
4467
5510
|
multi_az: asBoolean2(props.multiAz)
|
|
4468
5511
|
};
|
|
4469
5512
|
case "aws:s3/bucket:Bucket":
|
|
@@ -4471,8 +5514,8 @@ function extractAttributes3(pulumiType, props) {
|
|
|
4471
5514
|
case "aws:ebs/volume:Volume":
|
|
4472
5515
|
return {
|
|
4473
5516
|
storage_type: asString2(props.type),
|
|
4474
|
-
storage_size_gb:
|
|
4475
|
-
iops:
|
|
5517
|
+
storage_size_gb: num(props.size),
|
|
5518
|
+
iops: num(props.iops)
|
|
4476
5519
|
};
|
|
4477
5520
|
case "aws:lb/loadBalancer:LoadBalancer":
|
|
4478
5521
|
return {
|
|
@@ -4484,14 +5527,14 @@ function extractAttributes3(pulumiType, props) {
|
|
|
4484
5527
|
return {};
|
|
4485
5528
|
case "aws:lambda/function:Function":
|
|
4486
5529
|
return {
|
|
4487
|
-
memory_size:
|
|
4488
|
-
timeout:
|
|
5530
|
+
memory_size: num(props.memorySize),
|
|
5531
|
+
timeout: num(props.timeout)
|
|
4489
5532
|
};
|
|
4490
5533
|
case "aws:dynamodb/table:Table":
|
|
4491
5534
|
return {
|
|
4492
5535
|
billing_mode: asString2(props.billingMode) ?? "PROVISIONED",
|
|
4493
|
-
read_capacity:
|
|
4494
|
-
write_capacity:
|
|
5536
|
+
read_capacity: num(props.readCapacity),
|
|
5537
|
+
write_capacity: num(props.writeCapacity)
|
|
4495
5538
|
};
|
|
4496
5539
|
case "aws:route53/zone:Zone":
|
|
4497
5540
|
return {};
|
|
@@ -4537,12 +5580,12 @@ function extractAttributes3(pulumiType, props) {
|
|
|
4537
5580
|
};
|
|
4538
5581
|
case "gcp:container:Cluster":
|
|
4539
5582
|
return {
|
|
4540
|
-
node_count:
|
|
5583
|
+
node_count: num(props.initialNodeCount)
|
|
4541
5584
|
};
|
|
4542
5585
|
case "gcp:cloudfunctions:Function":
|
|
4543
5586
|
return {
|
|
4544
|
-
memory_size:
|
|
4545
|
-
timeout:
|
|
5587
|
+
memory_size: num(props.availableMemoryMb),
|
|
5588
|
+
timeout: num(props.timeout)
|
|
4546
5589
|
};
|
|
4547
5590
|
case "gcp:dns:ManagedZone":
|
|
4548
5591
|
return {};
|
|
@@ -4565,14 +5608,6 @@ function extractTags3(props) {
|
|
|
4565
5608
|
function asString2(value) {
|
|
4566
5609
|
return typeof value === "string" ? value : void 0;
|
|
4567
5610
|
}
|
|
4568
|
-
function asNumber3(value) {
|
|
4569
|
-
if (typeof value === "number") return value;
|
|
4570
|
-
if (typeof value === "string") {
|
|
4571
|
-
const n = Number(value);
|
|
4572
|
-
return Number.isNaN(n) ? void 0 : n;
|
|
4573
|
-
}
|
|
4574
|
-
return void 0;
|
|
4575
|
-
}
|
|
4576
5611
|
function asBoolean2(value) {
|
|
4577
5612
|
if (typeof value === "boolean") return value;
|
|
4578
5613
|
if (value === "true") return true;
|
|
@@ -4595,7 +5630,7 @@ var PulumiParser = class {
|
|
|
4595
5630
|
}
|
|
4596
5631
|
});
|
|
4597
5632
|
}
|
|
4598
|
-
|
|
5633
|
+
parse(files, _options) {
|
|
4599
5634
|
const warnings = [];
|
|
4600
5635
|
const file = files.find((f) => {
|
|
4601
5636
|
try {
|
|
@@ -4607,7 +5642,7 @@ var PulumiParser = class {
|
|
|
4607
5642
|
}
|
|
4608
5643
|
});
|
|
4609
5644
|
if (!file) {
|
|
4610
|
-
return emptyInventory3(warnings);
|
|
5645
|
+
return Promise.resolve(emptyInventory3(warnings));
|
|
4611
5646
|
}
|
|
4612
5647
|
let stackExport;
|
|
4613
5648
|
try {
|
|
@@ -4616,11 +5651,11 @@ var PulumiParser = class {
|
|
|
4616
5651
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4617
5652
|
warnings.push(`Failed to parse Pulumi stack export ${file.path}: ${msg}`);
|
|
4618
5653
|
logger.warn("Pulumi parse error", { path: file.path, error: msg });
|
|
4619
|
-
return emptyInventory3(warnings);
|
|
5654
|
+
return Promise.resolve(emptyInventory3(warnings));
|
|
4620
5655
|
}
|
|
4621
5656
|
if (!stackExport?.deployment?.resources || !Array.isArray(stackExport.deployment.resources)) {
|
|
4622
5657
|
warnings.push(`Invalid Pulumi stack export: missing deployment.resources in ${file.path}`);
|
|
4623
|
-
return emptyInventory3(warnings);
|
|
5658
|
+
return Promise.resolve(emptyInventory3(warnings));
|
|
4624
5659
|
}
|
|
4625
5660
|
const resources = [];
|
|
4626
5661
|
for (const pulumiRes of stackExport.deployment.resources) {
|
|
@@ -4654,7 +5689,7 @@ var PulumiParser = class {
|
|
|
4654
5689
|
region: dominantRegion,
|
|
4655
5690
|
warningCount: warnings.length
|
|
4656
5691
|
});
|
|
4657
|
-
return inventory;
|
|
5692
|
+
return Promise.resolve(inventory);
|
|
4658
5693
|
}
|
|
4659
5694
|
};
|
|
4660
5695
|
function emptyInventory3(warnings) {
|
|
@@ -4751,7 +5786,7 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
|
|
|
4751
5786
|
parse_warnings: warnings
|
|
4752
5787
|
};
|
|
4753
5788
|
}
|
|
4754
|
-
const combined =
|
|
5789
|
+
const combined = mergeHclJsons(parsedJsons.map((p) => p.json));
|
|
4755
5790
|
const variables = resolveVariables(combined, tfvarsContent);
|
|
4756
5791
|
const providers = ["aws", "azure", "gcp"];
|
|
4757
5792
|
const defaultRegions = {};
|
|
@@ -4770,7 +5805,7 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
|
|
|
4770
5805
|
}
|
|
4771
5806
|
}
|
|
4772
5807
|
if (resolveModulesEnabled) {
|
|
4773
|
-
const moduleBasePath = basePath ?? (parsedJsons.length > 0 ?
|
|
5808
|
+
const moduleBasePath = basePath ?? (parsedJsons.length > 0 ? deriveModuleBasePath(parsedJsons[0].path) : process.cwd());
|
|
4774
5809
|
try {
|
|
4775
5810
|
const moduleResources = await resolveModules(combined, moduleBasePath, variables, warnings);
|
|
4776
5811
|
allResources.push(...moduleResources);
|
|
@@ -4803,46 +5838,19 @@ async function parseTerraform(files, tfvarsContent, basePath, resolveModulesEnab
|
|
|
4803
5838
|
});
|
|
4804
5839
|
return inventory;
|
|
4805
5840
|
}
|
|
4806
|
-
function
|
|
4807
|
-
const
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
continue;
|
|
4813
|
-
}
|
|
4814
|
-
const existing = merged[key];
|
|
4815
|
-
if (value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing)) {
|
|
4816
|
-
merged[key] = mergeObjects2(
|
|
4817
|
-
existing,
|
|
4818
|
-
value
|
|
4819
|
-
);
|
|
4820
|
-
} else if (Array.isArray(existing) && Array.isArray(value)) {
|
|
4821
|
-
merged[key] = [...existing, ...value];
|
|
4822
|
-
} else {
|
|
4823
|
-
merged[key] = deepClone(value);
|
|
4824
|
-
}
|
|
4825
|
-
}
|
|
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();
|
|
4826
5847
|
}
|
|
4827
|
-
|
|
4828
|
-
}
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
for (const [key, bVal] of Object.entries(b)) {
|
|
4832
|
-
const aVal = result[key];
|
|
4833
|
-
if (aVal !== null && typeof aVal === "object" && !Array.isArray(aVal) && bVal !== null && typeof bVal === "object" && !Array.isArray(bVal)) {
|
|
4834
|
-
result[key] = mergeObjects2(aVal, bVal);
|
|
4835
|
-
} else if (Array.isArray(aVal) && Array.isArray(bVal)) {
|
|
4836
|
-
result[key] = [...aVal, ...bVal];
|
|
4837
|
-
} else {
|
|
4838
|
-
result[key] = deepClone(bVal);
|
|
4839
|
-
}
|
|
5848
|
+
const dir = dirname2(normalized);
|
|
5849
|
+
const escapesUpward = dir === ".." || dir.startsWith(`..${sep2}`) || dir.startsWith("../");
|
|
5850
|
+
if (escapesUpward) {
|
|
5851
|
+
return process.cwd();
|
|
4840
5852
|
}
|
|
4841
|
-
return
|
|
4842
|
-
}
|
|
4843
|
-
function deepClone(value) {
|
|
4844
|
-
if (value === null || typeof value !== "object") return value;
|
|
4845
|
-
return structuredClone(value);
|
|
5853
|
+
return dir;
|
|
4846
5854
|
}
|
|
4847
5855
|
|
|
4848
5856
|
// src/parsers/dependency-graph.ts
|
|
@@ -4973,16 +5981,54 @@ function generateMermaidDiagram(graph) {
|
|
|
4973
5981
|
return lines.join("\n");
|
|
4974
5982
|
}
|
|
4975
5983
|
|
|
5984
|
+
// src/schemas/fragments.ts
|
|
5985
|
+
import { z as z2 } from "zod";
|
|
5986
|
+
|
|
5987
|
+
// src/schemas/bounded.ts
|
|
5988
|
+
import { z } from "zod";
|
|
5989
|
+
var MAX_FILE_PATH_LEN = 1024;
|
|
5990
|
+
var MAX_FILE_CONTENT_BYTES = 5 * 1024 * 1024;
|
|
5991
|
+
var MAX_TFVARS_BYTES = 1 * 1024 * 1024;
|
|
5992
|
+
var MAX_PLAN_JSON_BYTES = 20 * 1024 * 1024;
|
|
5993
|
+
var MAX_STATE_JSON_BYTES = 20 * 1024 * 1024;
|
|
5994
|
+
var MAX_SHORT_STRING_LEN = 256;
|
|
5995
|
+
var MAX_TOTAL_FILE_BYTES = 50 * 1024 * 1024;
|
|
5996
|
+
var filePathSchema = z.string().max(MAX_FILE_PATH_LEN, `File path exceeds ${MAX_FILE_PATH_LEN} characters`);
|
|
5997
|
+
var fileContentSchema = z.string().max(MAX_FILE_CONTENT_BYTES, `File content exceeds ${MAX_FILE_CONTENT_BYTES} bytes`);
|
|
5998
|
+
var tfvarsSchema = z.string().max(MAX_TFVARS_BYTES, `tfvars content exceeds ${MAX_TFVARS_BYTES} bytes`);
|
|
5999
|
+
var planJsonSchema = z.string().max(MAX_PLAN_JSON_BYTES, `plan JSON exceeds ${MAX_PLAN_JSON_BYTES} bytes`);
|
|
6000
|
+
var stateJsonSchema = z.string().max(MAX_STATE_JSON_BYTES, `state JSON exceeds ${MAX_STATE_JSON_BYTES} bytes`);
|
|
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.");
|
|
6026
|
+
|
|
4976
6027
|
// src/tools/analyze-terraform.ts
|
|
4977
|
-
var analyzeTerraformSchema =
|
|
4978
|
-
files:
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
content: z.string().describe("File content (HCL)")
|
|
4982
|
-
})
|
|
4983
|
-
).describe("Terraform files to analyze"),
|
|
4984
|
-
tfvars: z.string().optional().describe("Contents of terraform.tfvars file"),
|
|
4985
|
-
include_dependencies: z.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(
|
|
4986
6032
|
"When true, parse resource references and depends_on blocks to build a dependency graph and Mermaid diagram"
|
|
4987
6033
|
)
|
|
4988
6034
|
});
|
|
@@ -5034,7 +6080,7 @@ async function analyzeTerraform(params) {
|
|
|
5034
6080
|
}
|
|
5035
6081
|
|
|
5036
6082
|
// src/tools/estimate-cost.ts
|
|
5037
|
-
import { z as
|
|
6083
|
+
import { z as z4 } from "zod";
|
|
5038
6084
|
|
|
5039
6085
|
// src/mapping/region-mapper.ts
|
|
5040
6086
|
var DEFAULT_REGIONS = {
|
|
@@ -5063,6 +6109,27 @@ function mapRegion(region, sourceProvider, targetProvider) {
|
|
|
5063
6109
|
return mapped;
|
|
5064
6110
|
}
|
|
5065
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
|
+
|
|
5066
6133
|
// src/mapping/instance-mapper.ts
|
|
5067
6134
|
function directionKey(source, target) {
|
|
5068
6135
|
if (source === target) return null;
|
|
@@ -5244,7 +6311,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
5244
6311
|
`No instance mapping found for ${sourceInstanceType} (${resource.provider} -> ${targetProvider})`
|
|
5245
6312
|
);
|
|
5246
6313
|
} else {
|
|
5247
|
-
const { getInstanceMap: getInstanceMap2 } = await import("./loader-
|
|
6314
|
+
const { getInstanceMap: getInstanceMap2 } = await import("./loader-CMBV7346.js");
|
|
5248
6315
|
const im = getInstanceMap2();
|
|
5249
6316
|
const dirKey = `${resource.provider}_to_${targetProvider}`;
|
|
5250
6317
|
const directMap = im[dirKey];
|
|
@@ -5258,11 +6325,7 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
5258
6325
|
}
|
|
5259
6326
|
}
|
|
5260
6327
|
const effectiveInstance = mappedInstance ?? sourceInstanceType;
|
|
5261
|
-
const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(
|
|
5262
|
-
effectiveInstance,
|
|
5263
|
-
targetRegion,
|
|
5264
|
-
resource.attributes.os
|
|
5265
|
-
) : null;
|
|
6328
|
+
const computePrice = effectiveInstance ? await pricingEngine.getProvider(targetProvider).getComputePrice(effectiveInstance, targetRegion, resource.attributes.os) : null;
|
|
5266
6329
|
let computeMonthlyCost = 0;
|
|
5267
6330
|
let pricingSource = "fallback";
|
|
5268
6331
|
if (computePrice) {
|
|
@@ -5282,13 +6345,69 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
5282
6345
|
}
|
|
5283
6346
|
let hourlyPrice = computePrice.price_per_unit;
|
|
5284
6347
|
if (pricingConfig.pricing_model === "spot") {
|
|
5285
|
-
|
|
5286
|
-
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5291
|
-
|
|
6348
|
+
let liveSpotApplied = false;
|
|
6349
|
+
if (targetProvider === "azure") {
|
|
6350
|
+
try {
|
|
6351
|
+
const azureProvider = pricingEngine.getProvider("azure");
|
|
6352
|
+
if (typeof azureProvider.getSpotPrice === "function") {
|
|
6353
|
+
const liveSpot = await azureProvider.getSpotPrice(
|
|
6354
|
+
effectiveInstance,
|
|
6355
|
+
targetRegion,
|
|
6356
|
+
resource.attributes.os
|
|
6357
|
+
);
|
|
6358
|
+
if (liveSpot && liveSpot.price_per_unit > 0 && hourlyPrice > 0) {
|
|
6359
|
+
const savingsPct = Math.max(
|
|
6360
|
+
0,
|
|
6361
|
+
Math.round((1 - liveSpot.price_per_unit / hourlyPrice) * 100)
|
|
6362
|
+
);
|
|
6363
|
+
hourlyPrice = liveSpot.price_per_unit;
|
|
6364
|
+
pricingSource = "live";
|
|
6365
|
+
liveSpotApplied = true;
|
|
6366
|
+
notes.push(
|
|
6367
|
+
`Spot pricing applied from live Azure Retail API (${savingsPct}% discount from on-demand)`
|
|
6368
|
+
);
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6371
|
+
} catch (err) {
|
|
6372
|
+
logger.debug("Azure live spot lookup failed, using fallback discount factor", {
|
|
6373
|
+
err: err instanceof Error ? err.message : String(err)
|
|
6374
|
+
});
|
|
6375
|
+
}
|
|
6376
|
+
}
|
|
6377
|
+
if (!liveSpotApplied && targetProvider === "aws") {
|
|
6378
|
+
try {
|
|
6379
|
+
const awsProvider = pricingEngine.getProvider("aws");
|
|
6380
|
+
if (typeof awsProvider.getSpotFactor === "function") {
|
|
6381
|
+
const liveFactor = await awsProvider.getSpotFactor(
|
|
6382
|
+
effectiveInstance,
|
|
6383
|
+
targetRegion,
|
|
6384
|
+
resource.attributes.os
|
|
6385
|
+
);
|
|
6386
|
+
if (liveFactor !== null && liveFactor > 0 && liveFactor < 1) {
|
|
6387
|
+
const savingsPct = Math.round((1 - liveFactor) * 100);
|
|
6388
|
+
hourlyPrice = hourlyPrice * liveFactor;
|
|
6389
|
+
pricingSource = "live";
|
|
6390
|
+
liveSpotApplied = true;
|
|
6391
|
+
notes.push(
|
|
6392
|
+
`Spot pricing applied from live AWS Spot Advisor (${savingsPct}% discount from on-demand)`
|
|
6393
|
+
);
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
} catch (err) {
|
|
6397
|
+
logger.debug("AWS live spot lookup failed, using fallback discount factor", {
|
|
6398
|
+
err: err instanceof Error ? err.message : String(err)
|
|
6399
|
+
});
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
if (!liveSpotApplied) {
|
|
6403
|
+
const family = classifyInstanceFamily(effectiveInstance, targetProvider);
|
|
6404
|
+
const providerFactors = SPOT_DISCOUNT_FACTORS[targetProvider] ?? SPOT_DISCOUNT_FACTORS.aws;
|
|
6405
|
+
const factor = providerFactors[family] ?? providerFactors.general ?? 0.35;
|
|
6406
|
+
const savingsPct = Math.round((1 - factor) * 100);
|
|
6407
|
+
hourlyPrice = hourlyPrice * factor;
|
|
6408
|
+
pricingSource = "spot-estimate";
|
|
6409
|
+
notes.push(`Spot pricing applied (${savingsPct}% discount from on-demand)`);
|
|
6410
|
+
}
|
|
5292
6411
|
}
|
|
5293
6412
|
computeMonthlyCost = hourlyPrice * monthlyHours;
|
|
5294
6413
|
breakdown.push({
|
|
@@ -5327,20 +6446,16 @@ async function calculateComputeCost(resource, targetProvider, targetRegion, pric
|
|
|
5327
6446
|
}
|
|
5328
6447
|
}
|
|
5329
6448
|
const totalMonthly = computeMonthlyCost + storageMonthlyCost;
|
|
5330
|
-
return {
|
|
5331
|
-
|
|
5332
|
-
resource_type: resource.type,
|
|
5333
|
-
resource_name: resource.name,
|
|
6449
|
+
return makeCostEstimate({
|
|
6450
|
+
resource,
|
|
5334
6451
|
provider: targetProvider,
|
|
5335
6452
|
region: targetRegion,
|
|
5336
|
-
|
|
5337
|
-
yearly_cost: totalMonthly * 12,
|
|
5338
|
-
currency: "USD",
|
|
6453
|
+
monthlyCost: totalMonthly,
|
|
5339
6454
|
breakdown,
|
|
5340
6455
|
confidence,
|
|
5341
6456
|
notes,
|
|
5342
|
-
|
|
5343
|
-
};
|
|
6457
|
+
pricingSource
|
|
6458
|
+
});
|
|
5344
6459
|
}
|
|
5345
6460
|
|
|
5346
6461
|
// src/calculator/database.ts
|
|
@@ -5443,20 +6558,16 @@ async function calculateDatabaseCost(resource, targetProvider, targetRegion, pri
|
|
|
5443
6558
|
}
|
|
5444
6559
|
}
|
|
5445
6560
|
const totalMonthly = instanceMonthlyCost + storageMonthlyCost;
|
|
5446
|
-
return {
|
|
5447
|
-
|
|
5448
|
-
resource_type: resource.type,
|
|
5449
|
-
resource_name: resource.name,
|
|
6561
|
+
return makeCostEstimate({
|
|
6562
|
+
resource,
|
|
5450
6563
|
provider: targetProvider,
|
|
5451
6564
|
region: targetRegion,
|
|
5452
|
-
|
|
5453
|
-
yearly_cost: totalMonthly * 12,
|
|
5454
|
-
currency: "USD",
|
|
6565
|
+
monthlyCost: totalMonthly,
|
|
5455
6566
|
breakdown,
|
|
5456
6567
|
confidence: mappedInstance ? "high" : "low",
|
|
5457
6568
|
notes,
|
|
5458
|
-
|
|
5459
|
-
};
|
|
6569
|
+
pricingSource
|
|
6570
|
+
});
|
|
5460
6571
|
}
|
|
5461
6572
|
|
|
5462
6573
|
// src/calculator/storage.ts
|
|
@@ -5535,20 +6646,16 @@ async function calculateStorageCost(resource, targetProvider, targetRegion, pric
|
|
|
5535
6646
|
targetRegion
|
|
5536
6647
|
});
|
|
5537
6648
|
}
|
|
5538
|
-
return {
|
|
5539
|
-
|
|
5540
|
-
resource_type: resource.type,
|
|
5541
|
-
resource_name: resource.name,
|
|
6649
|
+
return makeCostEstimate({
|
|
6650
|
+
resource,
|
|
5542
6651
|
provider: targetProvider,
|
|
5543
6652
|
region: targetRegion,
|
|
5544
|
-
|
|
5545
|
-
yearly_cost: totalMonthly * 12,
|
|
5546
|
-
currency: "USD",
|
|
6653
|
+
monthlyCost: totalMonthly,
|
|
5547
6654
|
breakdown,
|
|
5548
6655
|
confidence: storagePrice ? "high" : "low",
|
|
5549
6656
|
notes,
|
|
5550
|
-
|
|
5551
|
-
};
|
|
6657
|
+
pricingSource
|
|
6658
|
+
});
|
|
5552
6659
|
}
|
|
5553
6660
|
|
|
5554
6661
|
// src/calculator/network.ts
|
|
@@ -5598,20 +6705,16 @@ async function calculateNatGatewayCost(resource, targetProvider, targetRegion, p
|
|
|
5598
6705
|
} else {
|
|
5599
6706
|
notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
|
|
5600
6707
|
}
|
|
5601
|
-
return {
|
|
5602
|
-
|
|
5603
|
-
resource_type: resource.type,
|
|
5604
|
-
resource_name: resource.name,
|
|
6708
|
+
return makeCostEstimate({
|
|
6709
|
+
resource,
|
|
5605
6710
|
provider: targetProvider,
|
|
5606
6711
|
region: targetRegion,
|
|
5607
|
-
|
|
5608
|
-
yearly_cost: totalMonthly * 12,
|
|
5609
|
-
currency: "USD",
|
|
6712
|
+
monthlyCost: totalMonthly,
|
|
5610
6713
|
breakdown,
|
|
5611
6714
|
confidence: natPrice ? "medium" : "low",
|
|
5612
6715
|
notes,
|
|
5613
|
-
|
|
5614
|
-
};
|
|
6716
|
+
pricingSource: natPricingSource
|
|
6717
|
+
});
|
|
5615
6718
|
}
|
|
5616
6719
|
async function calculateLoadBalancerCost(resource, targetProvider, targetRegion, pricingEngine, monthlyHours = 730) {
|
|
5617
6720
|
const notes = [];
|
|
@@ -5643,20 +6746,16 @@ async function calculateLoadBalancerCost(resource, targetProvider, targetRegion,
|
|
|
5643
6746
|
} else {
|
|
5644
6747
|
notes.push(`No pricing data found for ${resource.type} in ${targetRegion}`);
|
|
5645
6748
|
}
|
|
5646
|
-
return {
|
|
5647
|
-
|
|
5648
|
-
resource_type: resource.type,
|
|
5649
|
-
resource_name: resource.name,
|
|
6749
|
+
return makeCostEstimate({
|
|
6750
|
+
resource,
|
|
5650
6751
|
provider: targetProvider,
|
|
5651
6752
|
region: targetRegion,
|
|
5652
|
-
|
|
5653
|
-
yearly_cost: totalMonthly * 12,
|
|
5654
|
-
currency: "USD",
|
|
6753
|
+
monthlyCost: totalMonthly,
|
|
5655
6754
|
breakdown,
|
|
5656
6755
|
confidence: lbPrice ? "medium" : "low",
|
|
5657
6756
|
notes,
|
|
5658
|
-
|
|
5659
|
-
};
|
|
6757
|
+
pricingSource: lbPricingSource
|
|
6758
|
+
});
|
|
5660
6759
|
}
|
|
5661
6760
|
|
|
5662
6761
|
// src/calculator/kubernetes.ts
|
|
@@ -5727,20 +6826,16 @@ async function calculateKubernetesCost(resource, targetProvider, targetRegion, p
|
|
|
5727
6826
|
);
|
|
5728
6827
|
}
|
|
5729
6828
|
}
|
|
5730
|
-
return {
|
|
5731
|
-
|
|
5732
|
-
resource_type: resource.type,
|
|
5733
|
-
resource_name: resource.name,
|
|
6829
|
+
return makeCostEstimate({
|
|
6830
|
+
resource,
|
|
5734
6831
|
provider: targetProvider,
|
|
5735
6832
|
region: targetRegion,
|
|
5736
|
-
|
|
5737
|
-
yearly_cost: totalMonthly * 12,
|
|
5738
|
-
currency: "USD",
|
|
6833
|
+
monthlyCost: totalMonthly,
|
|
5739
6834
|
breakdown,
|
|
5740
6835
|
confidence: k8sPrice ? "medium" : "low",
|
|
5741
6836
|
notes,
|
|
5742
|
-
|
|
5743
|
-
};
|
|
6837
|
+
pricingSource
|
|
6838
|
+
});
|
|
5744
6839
|
}
|
|
5745
6840
|
|
|
5746
6841
|
// src/calculator/serverless.ts
|
|
@@ -5795,20 +6890,16 @@ function calculateLambdaCost(resource, targetProvider, targetRegion) {
|
|
|
5795
6890
|
}
|
|
5796
6891
|
const totalMonthly = requestCost + computeCost;
|
|
5797
6892
|
void timeoutSec;
|
|
5798
|
-
return {
|
|
5799
|
-
|
|
5800
|
-
resource_type: resource.type,
|
|
5801
|
-
resource_name: resource.name,
|
|
6893
|
+
return makeCostEstimate({
|
|
6894
|
+
resource,
|
|
5802
6895
|
provider: targetProvider,
|
|
5803
6896
|
region: targetRegion,
|
|
5804
|
-
|
|
5805
|
-
yearly_cost: totalMonthly * 12,
|
|
5806
|
-
currency: "USD",
|
|
6897
|
+
monthlyCost: totalMonthly,
|
|
5807
6898
|
breakdown,
|
|
5808
6899
|
confidence: "medium",
|
|
5809
6900
|
notes,
|
|
5810
|
-
|
|
5811
|
-
};
|
|
6901
|
+
pricingSource: "fallback"
|
|
6902
|
+
});
|
|
5812
6903
|
}
|
|
5813
6904
|
var DYNAMO_ON_DEMAND_WRITE_PER_MILLION = 1.25;
|
|
5814
6905
|
var DYNAMO_ON_DEMAND_READ_PER_MILLION = 0.25;
|
|
@@ -5878,20 +6969,16 @@ function calculateDynamoDbCost(resource, targetProvider, targetRegion) {
|
|
|
5878
6969
|
monthly_cost: storageCost
|
|
5879
6970
|
});
|
|
5880
6971
|
totalMonthly += storageCost;
|
|
5881
|
-
return {
|
|
5882
|
-
|
|
5883
|
-
resource_type: resource.type,
|
|
5884
|
-
resource_name: resource.name,
|
|
6972
|
+
return makeCostEstimate({
|
|
6973
|
+
resource,
|
|
5885
6974
|
provider: targetProvider,
|
|
5886
6975
|
region: targetRegion,
|
|
5887
|
-
|
|
5888
|
-
yearly_cost: totalMonthly * 12,
|
|
5889
|
-
currency: "USD",
|
|
6976
|
+
monthlyCost: totalMonthly,
|
|
5890
6977
|
breakdown,
|
|
5891
6978
|
confidence: "medium",
|
|
5892
6979
|
notes,
|
|
5893
|
-
|
|
5894
|
-
};
|
|
6980
|
+
pricingSource: "fallback"
|
|
6981
|
+
});
|
|
5895
6982
|
}
|
|
5896
6983
|
var SQS_STANDARD_PER_MILLION = 0.4;
|
|
5897
6984
|
var SQS_FIFO_PER_MILLION = 0.5;
|
|
@@ -5916,20 +7003,16 @@ function calculateSqsCost(resource, targetProvider, targetRegion) {
|
|
|
5916
7003
|
);
|
|
5917
7004
|
}
|
|
5918
7005
|
notes.push(`SQS ${queueType} queue pricing at $${pricePerMillion}/million requests`);
|
|
5919
|
-
return {
|
|
5920
|
-
|
|
5921
|
-
resource_type: resource.type,
|
|
5922
|
-
resource_name: resource.name,
|
|
7006
|
+
return makeCostEstimate({
|
|
7007
|
+
resource,
|
|
5923
7008
|
provider: targetProvider,
|
|
5924
7009
|
region: targetRegion,
|
|
5925
|
-
|
|
5926
|
-
yearly_cost: totalMonthly * 12,
|
|
5927
|
-
currency: "USD",
|
|
7010
|
+
monthlyCost: totalMonthly,
|
|
5928
7011
|
breakdown,
|
|
5929
7012
|
confidence: "medium",
|
|
5930
7013
|
notes,
|
|
5931
|
-
|
|
5932
|
-
};
|
|
7014
|
+
pricingSource: "fallback"
|
|
7015
|
+
});
|
|
5933
7016
|
}
|
|
5934
7017
|
|
|
5935
7018
|
// src/calculator/cache.ts
|
|
@@ -5986,20 +7069,16 @@ function calculateElastiCacheCost(resource, targetProvider, targetRegion) {
|
|
|
5986
7069
|
} else {
|
|
5987
7070
|
notes.push(`No pricing data found for ElastiCache node type ${nodeType}; cost reported as $0`);
|
|
5988
7071
|
}
|
|
5989
|
-
return {
|
|
5990
|
-
|
|
5991
|
-
resource_type: resource.type,
|
|
5992
|
-
resource_name: resource.name,
|
|
7072
|
+
return makeCostEstimate({
|
|
7073
|
+
resource,
|
|
5993
7074
|
provider: targetProvider,
|
|
5994
7075
|
region: targetRegion,
|
|
5995
|
-
|
|
5996
|
-
yearly_cost: totalMonthly * 12,
|
|
5997
|
-
currency: "USD",
|
|
7076
|
+
monthlyCost: totalMonthly,
|
|
5998
7077
|
breakdown,
|
|
5999
7078
|
confidence: basePrice !== void 0 ? "medium" : "low",
|
|
6000
7079
|
notes,
|
|
6001
|
-
|
|
6002
|
-
};
|
|
7080
|
+
pricingSource
|
|
7081
|
+
});
|
|
6003
7082
|
}
|
|
6004
7083
|
var AZURE_REDIS_PRICES = {
|
|
6005
7084
|
// Basic (single node, no SLA)
|
|
@@ -6052,20 +7131,16 @@ function calculateAzureRedisCacheCost(resource, targetProvider, targetRegion) {
|
|
|
6052
7131
|
`No pricing found for Azure Redis Cache ${sku} ${capacityStr.toUpperCase()}; cost reported as $0`
|
|
6053
7132
|
);
|
|
6054
7133
|
}
|
|
6055
|
-
return {
|
|
6056
|
-
|
|
6057
|
-
resource_type: resource.type,
|
|
6058
|
-
resource_name: resource.name,
|
|
7134
|
+
return makeCostEstimate({
|
|
7135
|
+
resource,
|
|
6059
7136
|
provider: targetProvider,
|
|
6060
7137
|
region: targetRegion,
|
|
6061
|
-
|
|
6062
|
-
yearly_cost: totalMonthly * 12,
|
|
6063
|
-
currency: "USD",
|
|
7138
|
+
monthlyCost: totalMonthly,
|
|
6064
7139
|
breakdown,
|
|
6065
7140
|
confidence: hourlyPrice !== void 0 ? "medium" : "low",
|
|
6066
7141
|
notes,
|
|
6067
|
-
|
|
6068
|
-
};
|
|
7142
|
+
pricingSource
|
|
7143
|
+
});
|
|
6069
7144
|
}
|
|
6070
7145
|
var GCP_REDIS_PRICES = {
|
|
6071
7146
|
basic: 0.049,
|
|
@@ -6089,20 +7164,16 @@ function calculateGcpRedisCost(resource, targetProvider, targetRegion) {
|
|
|
6089
7164
|
notes.push(
|
|
6090
7165
|
`GCP Memorystore Redis: ${normalizedTier} tier, ${memorySizeGb}GB \u2014 $${pricePerGbHour}/GB/hr`
|
|
6091
7166
|
);
|
|
6092
|
-
return {
|
|
6093
|
-
|
|
6094
|
-
resource_type: resource.type,
|
|
6095
|
-
resource_name: resource.name,
|
|
7167
|
+
return makeCostEstimate({
|
|
7168
|
+
resource,
|
|
6096
7169
|
provider: targetProvider,
|
|
6097
7170
|
region: targetRegion,
|
|
6098
|
-
|
|
6099
|
-
yearly_cost: totalMonthly * 12,
|
|
6100
|
-
currency: "USD",
|
|
7171
|
+
monthlyCost: totalMonthly,
|
|
6101
7172
|
breakdown,
|
|
6102
7173
|
confidence: "medium",
|
|
6103
7174
|
notes,
|
|
6104
|
-
|
|
6105
|
-
};
|
|
7175
|
+
pricingSource: "fallback"
|
|
7176
|
+
});
|
|
6106
7177
|
}
|
|
6107
7178
|
var CLOUDFRONT_DATA_FIRST_10TB_PER_GB = 0.085;
|
|
6108
7179
|
var DEFAULT_CLOUDFRONT_TRANSFER_GB = 100;
|
|
@@ -6142,20 +7213,16 @@ function calculateCloudFrontCost(resource, targetProvider, targetRegion) {
|
|
|
6142
7213
|
notes.push(
|
|
6143
7214
|
`CloudFront ${priceClass} \u2014 HTTPS request charges excluded (runtime traffic data required)`
|
|
6144
7215
|
);
|
|
6145
|
-
return {
|
|
6146
|
-
|
|
6147
|
-
resource_type: resource.type,
|
|
6148
|
-
resource_name: resource.name,
|
|
7216
|
+
return makeCostEstimate({
|
|
7217
|
+
resource,
|
|
6149
7218
|
provider: targetProvider,
|
|
6150
7219
|
region: targetRegion,
|
|
6151
|
-
|
|
6152
|
-
yearly_cost: totalMonthly * 12,
|
|
6153
|
-
currency: "USD",
|
|
7220
|
+
monthlyCost: totalMonthly,
|
|
6154
7221
|
breakdown,
|
|
6155
7222
|
confidence: "medium",
|
|
6156
7223
|
notes,
|
|
6157
|
-
|
|
6158
|
-
};
|
|
7224
|
+
pricingSource: "fallback"
|
|
7225
|
+
});
|
|
6159
7226
|
}
|
|
6160
7227
|
|
|
6161
7228
|
// src/calculator/paas.ts
|
|
@@ -6222,20 +7289,16 @@ function calculateAppServicePlanCost(resource, targetProvider, targetRegion) {
|
|
|
6222
7289
|
`No pricing found for App Service Plan SKU "${skuName || skuSize || skuTier}"; cost reported as $0`
|
|
6223
7290
|
);
|
|
6224
7291
|
}
|
|
6225
|
-
return {
|
|
6226
|
-
|
|
6227
|
-
resource_type: resource.type,
|
|
6228
|
-
resource_name: resource.name,
|
|
7292
|
+
return makeCostEstimate({
|
|
7293
|
+
resource,
|
|
6229
7294
|
provider: targetProvider,
|
|
6230
7295
|
region: targetRegion,
|
|
6231
|
-
|
|
6232
|
-
yearly_cost: totalMonthly * 12,
|
|
6233
|
-
currency: "USD",
|
|
7296
|
+
monthlyCost: totalMonthly,
|
|
6234
7297
|
breakdown,
|
|
6235
7298
|
confidence: hourlyPrice !== void 0 ? "medium" : "low",
|
|
6236
7299
|
notes,
|
|
6237
|
-
|
|
6238
|
-
};
|
|
7300
|
+
pricingSource
|
|
7301
|
+
});
|
|
6239
7302
|
}
|
|
6240
7303
|
var COSMOS_SERVERLESS_PER_MILLION_RU = 0.25;
|
|
6241
7304
|
var COSMOS_PROVISIONED_PER_100_RU_PER_HR = 8e-3;
|
|
@@ -6293,20 +7356,16 @@ function calculateCosmosDbCost(resource, targetProvider, targetRegion) {
|
|
|
6293
7356
|
});
|
|
6294
7357
|
totalMonthly += storageCost;
|
|
6295
7358
|
}
|
|
6296
|
-
return {
|
|
6297
|
-
|
|
6298
|
-
resource_type: resource.type,
|
|
6299
|
-
resource_name: resource.name,
|
|
7359
|
+
return makeCostEstimate({
|
|
7360
|
+
resource,
|
|
6300
7361
|
provider: targetProvider,
|
|
6301
7362
|
region: targetRegion,
|
|
6302
|
-
|
|
6303
|
-
yearly_cost: totalMonthly * 12,
|
|
6304
|
-
currency: "USD",
|
|
7363
|
+
monthlyCost: totalMonthly,
|
|
6305
7364
|
breakdown,
|
|
6306
7365
|
confidence: "medium",
|
|
6307
7366
|
notes,
|
|
6308
|
-
|
|
6309
|
-
};
|
|
7367
|
+
pricingSource
|
|
7368
|
+
});
|
|
6310
7369
|
}
|
|
6311
7370
|
var AZURE_FUNC_PER_MILLION_EXECUTIONS = 0.2;
|
|
6312
7371
|
var AZURE_FUNC_GB_SECOND_PRICE = 16e-6;
|
|
@@ -6343,20 +7402,16 @@ function calculateAzureFunctionCost(resource, targetProvider, targetRegion) {
|
|
|
6343
7402
|
);
|
|
6344
7403
|
}
|
|
6345
7404
|
notes.push("Azure Function App on Consumption plan (dedicated plan not priced separately)");
|
|
6346
|
-
return {
|
|
6347
|
-
|
|
6348
|
-
resource_type: resource.type,
|
|
6349
|
-
resource_name: resource.name,
|
|
7405
|
+
return makeCostEstimate({
|
|
7406
|
+
resource,
|
|
6350
7407
|
provider: targetProvider,
|
|
6351
7408
|
region: targetRegion,
|
|
6352
|
-
|
|
6353
|
-
yearly_cost: totalMonthly * 12,
|
|
6354
|
-
currency: "USD",
|
|
7409
|
+
monthlyCost: totalMonthly,
|
|
6355
7410
|
breakdown,
|
|
6356
7411
|
confidence: "medium",
|
|
6357
7412
|
notes,
|
|
6358
|
-
|
|
6359
|
-
};
|
|
7413
|
+
pricingSource: "fallback"
|
|
7414
|
+
});
|
|
6360
7415
|
}
|
|
6361
7416
|
var CLOUD_RUN_VCPU_PER_SECOND = 24e-6;
|
|
6362
7417
|
var CLOUD_RUN_MEMORY_GIB_PER_SECOND = 25e-7;
|
|
@@ -6429,20 +7484,16 @@ function calculateCloudRunCost(resource, targetProvider, targetRegion) {
|
|
|
6429
7484
|
);
|
|
6430
7485
|
}
|
|
6431
7486
|
notes.push(`Cloud Run: ${vcpus} vCPU, ${memoryGib.toFixed(2)}GiB memory`);
|
|
6432
|
-
return {
|
|
6433
|
-
|
|
6434
|
-
resource_type: resource.type,
|
|
6435
|
-
resource_name: resource.name,
|
|
7487
|
+
return makeCostEstimate({
|
|
7488
|
+
resource,
|
|
6436
7489
|
provider: targetProvider,
|
|
6437
7490
|
region: targetRegion,
|
|
6438
|
-
|
|
6439
|
-
yearly_cost: totalMonthly * 12,
|
|
6440
|
-
currency: "USD",
|
|
7491
|
+
monthlyCost: totalMonthly,
|
|
6441
7492
|
breakdown,
|
|
6442
7493
|
confidence: "medium",
|
|
6443
7494
|
notes,
|
|
6444
|
-
|
|
6445
|
-
};
|
|
7495
|
+
pricingSource: "fallback"
|
|
7496
|
+
});
|
|
6446
7497
|
}
|
|
6447
7498
|
var BIGQUERY_QUERY_PER_TB = 5;
|
|
6448
7499
|
var BIGQUERY_STORAGE_PER_GB = 0.02;
|
|
@@ -6475,24 +7526,20 @@ function calculateBigQueryCost(resource, targetProvider, targetRegion) {
|
|
|
6475
7526
|
`Default assumption: ${DEFAULT_BIGQUERY_QUERY_TB}TB queries/month \u2014 provide monthly_query_tb attribute for accuracy`
|
|
6476
7527
|
);
|
|
6477
7528
|
}
|
|
6478
|
-
if (resource.attributes.location) {
|
|
7529
|
+
if (typeof resource.attributes.location === "string" && resource.attributes.location) {
|
|
6479
7530
|
notes.push(`BigQuery location: ${resource.attributes.location}`);
|
|
6480
7531
|
}
|
|
6481
7532
|
notes.push("BigQuery on-demand pricing; flat-rate/reservations not included");
|
|
6482
|
-
return {
|
|
6483
|
-
|
|
6484
|
-
resource_type: resource.type,
|
|
6485
|
-
resource_name: resource.name,
|
|
7533
|
+
return makeCostEstimate({
|
|
7534
|
+
resource,
|
|
6486
7535
|
provider: targetProvider,
|
|
6487
7536
|
region: targetRegion,
|
|
6488
|
-
|
|
6489
|
-
yearly_cost: totalMonthly * 12,
|
|
6490
|
-
currency: "USD",
|
|
7537
|
+
monthlyCost: totalMonthly,
|
|
6491
7538
|
breakdown,
|
|
6492
7539
|
confidence: "medium",
|
|
6493
7540
|
notes,
|
|
6494
|
-
|
|
6495
|
-
};
|
|
7541
|
+
pricingSource: "fallback"
|
|
7542
|
+
});
|
|
6496
7543
|
}
|
|
6497
7544
|
var GCF_PER_MILLION_INVOCATIONS = 0.4;
|
|
6498
7545
|
var GCF_GB_SECOND_PRICE = 25e-7;
|
|
@@ -6528,114 +7575,38 @@ function calculateGcpFunctionCost(resource, targetProvider, targetRegion) {
|
|
|
6528
7575
|
`Default assumption: ${DEFAULT_GCF_INVOCATIONS.toLocaleString()} invocations/month, ${DEFAULT_GCF_DURATION_MS}ms avg, ${DEFAULT_GCF_MEMORY_MB}MB memory`
|
|
6529
7576
|
);
|
|
6530
7577
|
}
|
|
6531
|
-
if (resource.attributes.runtime) {
|
|
7578
|
+
if (typeof resource.attributes.runtime === "string" && resource.attributes.runtime) {
|
|
6532
7579
|
notes.push(`Cloud Function runtime: ${resource.attributes.runtime}`);
|
|
6533
7580
|
}
|
|
6534
|
-
return {
|
|
6535
|
-
|
|
6536
|
-
resource_type: resource.type,
|
|
6537
|
-
resource_name: resource.name,
|
|
7581
|
+
return makeCostEstimate({
|
|
7582
|
+
resource,
|
|
6538
7583
|
provider: targetProvider,
|
|
6539
7584
|
region: targetRegion,
|
|
6540
|
-
|
|
6541
|
-
yearly_cost: totalMonthly * 12,
|
|
6542
|
-
currency: "USD",
|
|
7585
|
+
monthlyCost: totalMonthly,
|
|
6543
7586
|
breakdown,
|
|
6544
7587
|
confidence: "medium",
|
|
6545
7588
|
notes,
|
|
6546
|
-
|
|
6547
|
-
};
|
|
7589
|
+
pricingSource: "fallback"
|
|
7590
|
+
});
|
|
6548
7591
|
}
|
|
6549
7592
|
|
|
6550
7593
|
// src/calculator/data-transfer.ts
|
|
6551
7594
|
var DEFAULT_EGRESS_GB = 100;
|
|
6552
|
-
var
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
"ap-southeast-2": 1.11,
|
|
6566
|
-
"ap-northeast-1": 1.11,
|
|
6567
|
-
"ap-northeast-2": 1.11,
|
|
6568
|
-
"ap-south-1": 1.11,
|
|
6569
|
-
"sa-east-1": 1.31,
|
|
6570
|
-
// $0.118/GB
|
|
6571
|
-
"ca-central-1": 1,
|
|
6572
|
-
"eu-north-1": 1,
|
|
6573
|
-
"me-south-1": 1.11,
|
|
6574
|
-
"af-south-1": 1.16
|
|
6575
|
-
// $0.1046/GB
|
|
6576
|
-
};
|
|
6577
|
-
var AZURE_REGION_MULTIPLIERS = {
|
|
6578
|
-
eastus: 1,
|
|
6579
|
-
eastus2: 1,
|
|
6580
|
-
westus: 1,
|
|
6581
|
-
westus2: 1,
|
|
6582
|
-
westus3: 1,
|
|
6583
|
-
centralus: 1,
|
|
6584
|
-
northeurope: 1,
|
|
6585
|
-
westeurope: 1,
|
|
6586
|
-
uksouth: 1,
|
|
6587
|
-
ukwest: 1,
|
|
6588
|
-
germanywestcentral: 1,
|
|
6589
|
-
francecentral: 1,
|
|
6590
|
-
eastasia: 1.16,
|
|
6591
|
-
// Zone 2 rate
|
|
6592
|
-
southeastasia: 1.16,
|
|
6593
|
-
japaneast: 1.16,
|
|
6594
|
-
koreacentral: 1.16,
|
|
6595
|
-
australiaeast: 1.24,
|
|
6596
|
-
// Zone 3 rate
|
|
6597
|
-
brazilsouth: 1.31,
|
|
6598
|
-
southafricanorth: 1.31
|
|
6599
|
-
};
|
|
6600
|
-
var GCP_REGION_MULTIPLIERS = {
|
|
6601
|
-
"us-central1": 1,
|
|
6602
|
-
"us-east1": 1,
|
|
6603
|
-
"us-east4": 1,
|
|
6604
|
-
"us-west1": 1,
|
|
6605
|
-
"us-west2": 1,
|
|
6606
|
-
"us-west3": 1,
|
|
6607
|
-
"us-west4": 1,
|
|
6608
|
-
"europe-west1": 1,
|
|
6609
|
-
"europe-west2": 1,
|
|
6610
|
-
"europe-west3": 1,
|
|
6611
|
-
"europe-west4": 1,
|
|
6612
|
-
"europe-west6": 1,
|
|
6613
|
-
"europe-north1": 1,
|
|
6614
|
-
"northamerica-northeast1": 1,
|
|
6615
|
-
"southamerica-east1": 1.59,
|
|
6616
|
-
// $0.135/GB
|
|
6617
|
-
"asia-east1": 1.41,
|
|
6618
|
-
// $0.12/GB (APAC)
|
|
6619
|
-
"asia-east2": 1.41,
|
|
6620
|
-
"asia-northeast1": 1.41,
|
|
6621
|
-
"asia-northeast2": 1.41,
|
|
6622
|
-
"asia-northeast3": 1.41,
|
|
6623
|
-
"asia-south1": 1.41,
|
|
6624
|
-
"asia-southeast1": 1.41,
|
|
6625
|
-
"asia-southeast2": 1.41,
|
|
6626
|
-
"australia-southeast1": 1.41,
|
|
6627
|
-
"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
|
+
}
|
|
6628
7608
|
};
|
|
6629
|
-
function
|
|
6630
|
-
return AWS_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
|
|
6631
|
-
}
|
|
6632
|
-
function azureRegionMultiplier(region) {
|
|
6633
|
-
return AZURE_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
|
|
6634
|
-
}
|
|
6635
|
-
function gcpRegionMultiplier(region) {
|
|
6636
|
-
return GCP_REGION_MULTIPLIERS[region.toLowerCase()] ?? 1;
|
|
6637
|
-
}
|
|
6638
|
-
async function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
7609
|
+
function calculateEgressCost(resource, provider, targetRegion) {
|
|
6639
7610
|
const notes = [];
|
|
6640
7611
|
const breakdown = [];
|
|
6641
7612
|
const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
|
|
@@ -6644,8 +7615,8 @@ async function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
|
6644
7615
|
`Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
|
|
6645
7616
|
);
|
|
6646
7617
|
}
|
|
6647
|
-
const
|
|
6648
|
-
const ratePerGb =
|
|
7618
|
+
const pricing = EGRESS_PRICING[provider];
|
|
7619
|
+
const ratePerGb = pricing.ratePerGb * regionMultiplier(provider, targetRegion);
|
|
6649
7620
|
const totalMonthly = ratePerGb * egressGb;
|
|
6650
7621
|
breakdown.push({
|
|
6651
7622
|
description: `Internet egress (${egressGb} GB/month, estimated)`,
|
|
@@ -6654,97 +7625,26 @@ async function calculateAwsDataTransferCost(resource, targetRegion) {
|
|
|
6654
7625
|
unit_price: ratePerGb,
|
|
6655
7626
|
monthly_cost: totalMonthly
|
|
6656
7627
|
});
|
|
6657
|
-
notes.push(
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6661
|
-
resource_id: resource.id,
|
|
6662
|
-
resource_type: resource.type,
|
|
6663
|
-
resource_name: resource.name,
|
|
6664
|
-
provider: "aws",
|
|
7628
|
+
notes.push(pricing.note);
|
|
7629
|
+
return makeCostEstimate({
|
|
7630
|
+
resource,
|
|
7631
|
+
provider,
|
|
6665
7632
|
region: targetRegion,
|
|
6666
|
-
|
|
6667
|
-
yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
|
|
6668
|
-
currency: "USD",
|
|
7633
|
+
monthlyCost: totalMonthly,
|
|
6669
7634
|
breakdown,
|
|
6670
7635
|
confidence: "low",
|
|
6671
7636
|
notes,
|
|
6672
|
-
|
|
6673
|
-
};
|
|
6674
|
-
}
|
|
6675
|
-
async function calculateAzureDataTransferCost(resource, targetRegion) {
|
|
6676
|
-
const notes = [];
|
|
6677
|
-
const breakdown = [];
|
|
6678
|
-
const egressGb = resource.attributes.monthly_egress_gb ?? DEFAULT_EGRESS_GB;
|
|
6679
|
-
if (!resource.attributes.monthly_egress_gb || resource.attributes.monthly_egress_gb <= 0) {
|
|
6680
|
-
notes.push(
|
|
6681
|
-
`Data transfer cost estimated at ${egressGb} GB/month egress; actual costs depend on traffic volume and destination`
|
|
6682
|
-
);
|
|
6683
|
-
}
|
|
6684
|
-
const multiplier = azureRegionMultiplier(targetRegion);
|
|
6685
|
-
const ratePerGb = AZURE_EGRESS_PER_GB * multiplier;
|
|
6686
|
-
const totalMonthly = ratePerGb * egressGb;
|
|
6687
|
-
breakdown.push({
|
|
6688
|
-
description: `Internet egress (${egressGb} GB/month, estimated)`,
|
|
6689
|
-
unit: "GB",
|
|
6690
|
-
quantity: egressGb,
|
|
6691
|
-
unit_price: ratePerGb,
|
|
6692
|
-
monthly_cost: totalMonthly
|
|
7637
|
+
pricingSource: "fallback"
|
|
6693
7638
|
});
|
|
6694
|
-
notes.push(
|
|
6695
|
-
"Data transfer costs cover internet egress only; intra-region traffic is free on Azure and excluded from this estimate"
|
|
6696
|
-
);
|
|
6697
|
-
return {
|
|
6698
|
-
resource_id: resource.id,
|
|
6699
|
-
resource_type: resource.type,
|
|
6700
|
-
resource_name: resource.name,
|
|
6701
|
-
provider: "azure",
|
|
6702
|
-
region: targetRegion,
|
|
6703
|
-
monthly_cost: Math.round(totalMonthly * 100) / 100,
|
|
6704
|
-
yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
|
|
6705
|
-
currency: "USD",
|
|
6706
|
-
breakdown,
|
|
6707
|
-
confidence: "low",
|
|
6708
|
-
notes,
|
|
6709
|
-
pricing_source: "fallback"
|
|
6710
|
-
};
|
|
6711
7639
|
}
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
}
|
|
6721
|
-
const multiplier = gcpRegionMultiplier(targetRegion);
|
|
6722
|
-
const ratePerGb = GCP_EGRESS_PER_GB * multiplier;
|
|
6723
|
-
const totalMonthly = ratePerGb * egressGb;
|
|
6724
|
-
breakdown.push({
|
|
6725
|
-
description: `Internet egress (${egressGb} GB/month, estimated)`,
|
|
6726
|
-
unit: "GB",
|
|
6727
|
-
quantity: egressGb,
|
|
6728
|
-
unit_price: ratePerGb,
|
|
6729
|
-
monthly_cost: totalMonthly
|
|
6730
|
-
});
|
|
6731
|
-
notes.push(
|
|
6732
|
-
"Data transfer costs cover internet egress only; intra-region and Google network traffic charges are excluded from this estimate"
|
|
6733
|
-
);
|
|
6734
|
-
return {
|
|
6735
|
-
resource_id: resource.id,
|
|
6736
|
-
resource_type: resource.type,
|
|
6737
|
-
resource_name: resource.name,
|
|
6738
|
-
provider: "gcp",
|
|
6739
|
-
region: targetRegion,
|
|
6740
|
-
monthly_cost: Math.round(totalMonthly * 100) / 100,
|
|
6741
|
-
yearly_cost: Math.round(totalMonthly * 12 * 100) / 100,
|
|
6742
|
-
currency: "USD",
|
|
6743
|
-
breakdown,
|
|
6744
|
-
confidence: "low",
|
|
6745
|
-
notes,
|
|
6746
|
-
pricing_source: "fallback"
|
|
6747
|
-
};
|
|
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);
|
|
6748
7648
|
}
|
|
6749
7649
|
|
|
6750
7650
|
// src/calculator/container-registry.ts
|
|
@@ -6795,20 +7695,16 @@ function calculateContainerRegistryCost(resource, targetProvider, targetRegion)
|
|
|
6795
7695
|
monthly_cost: storageCost
|
|
6796
7696
|
});
|
|
6797
7697
|
const totalMonthly = storageCost;
|
|
6798
|
-
return {
|
|
6799
|
-
|
|
6800
|
-
resource_type: resource.type,
|
|
6801
|
-
resource_name: resource.name,
|
|
7698
|
+
return makeCostEstimate({
|
|
7699
|
+
resource,
|
|
6802
7700
|
provider: targetProvider,
|
|
6803
7701
|
region: targetRegion,
|
|
6804
|
-
|
|
6805
|
-
yearly_cost: totalMonthly * 12,
|
|
6806
|
-
currency: "USD",
|
|
7702
|
+
monthlyCost: totalMonthly,
|
|
6807
7703
|
breakdown,
|
|
6808
7704
|
confidence: hasExplicitStorage ? "high" : "medium",
|
|
6809
7705
|
notes,
|
|
6810
|
-
|
|
6811
|
-
};
|
|
7706
|
+
pricingSource: "bundled"
|
|
7707
|
+
});
|
|
6812
7708
|
}
|
|
6813
7709
|
|
|
6814
7710
|
// src/calculator/secrets.ts
|
|
@@ -6903,20 +7799,16 @@ function calculateSecretsCost(resource, targetProvider, targetRegion) {
|
|
|
6903
7799
|
});
|
|
6904
7800
|
totalMonthly += accessOpFee;
|
|
6905
7801
|
}
|
|
6906
|
-
return {
|
|
6907
|
-
|
|
6908
|
-
resource_type: resource.type,
|
|
6909
|
-
resource_name: resource.name,
|
|
7802
|
+
return makeCostEstimate({
|
|
7803
|
+
resource,
|
|
6910
7804
|
provider: targetProvider,
|
|
6911
7805
|
region: targetRegion,
|
|
6912
|
-
|
|
6913
|
-
yearly_cost: totalMonthly * 12,
|
|
6914
|
-
currency: "USD",
|
|
7806
|
+
monthlyCost: totalMonthly,
|
|
6915
7807
|
breakdown,
|
|
6916
7808
|
confidence: hasExplicitSecretCount ? "high" : "medium",
|
|
6917
7809
|
notes,
|
|
6918
|
-
|
|
6919
|
-
};
|
|
7810
|
+
pricingSource: "bundled"
|
|
7811
|
+
});
|
|
6920
7812
|
}
|
|
6921
7813
|
|
|
6922
7814
|
// src/calculator/dns.ts
|
|
@@ -6970,20 +7862,16 @@ function calculateDnsCost(resource, targetProvider, targetRegion) {
|
|
|
6970
7862
|
monthly_cost: queryCost
|
|
6971
7863
|
});
|
|
6972
7864
|
const totalMonthly = zonePrice + queryCost;
|
|
6973
|
-
return {
|
|
6974
|
-
|
|
6975
|
-
resource_type: resource.type,
|
|
6976
|
-
resource_name: resource.name,
|
|
7865
|
+
return makeCostEstimate({
|
|
7866
|
+
resource,
|
|
6977
7867
|
provider: targetProvider,
|
|
6978
7868
|
region: targetRegion,
|
|
6979
|
-
|
|
6980
|
-
yearly_cost: totalMonthly * 12,
|
|
6981
|
-
currency: "USD",
|
|
7869
|
+
monthlyCost: totalMonthly,
|
|
6982
7870
|
breakdown,
|
|
6983
7871
|
confidence: hasExplicitQueries ? "high" : "medium",
|
|
6984
7872
|
notes,
|
|
6985
|
-
|
|
6986
|
-
};
|
|
7873
|
+
pricingSource: "bundled"
|
|
7874
|
+
});
|
|
6987
7875
|
}
|
|
6988
7876
|
|
|
6989
7877
|
// src/calculator/api-gateway.ts
|
|
@@ -7104,20 +7992,16 @@ function calculateApiGatewayCost(resource, targetProvider, targetRegion) {
|
|
|
7104
7992
|
);
|
|
7105
7993
|
}
|
|
7106
7994
|
}
|
|
7107
|
-
return {
|
|
7108
|
-
|
|
7109
|
-
resource_type: resource.type,
|
|
7110
|
-
resource_name: resource.name,
|
|
7995
|
+
return makeCostEstimate({
|
|
7996
|
+
resource,
|
|
7111
7997
|
provider: targetProvider,
|
|
7112
7998
|
region: targetRegion,
|
|
7113
|
-
|
|
7114
|
-
yearly_cost: totalMonthly * 12,
|
|
7115
|
-
currency: "USD",
|
|
7999
|
+
monthlyCost: totalMonthly,
|
|
7116
8000
|
breakdown,
|
|
7117
8001
|
confidence: hasExplicitRequests ? "high" : "medium",
|
|
7118
8002
|
notes,
|
|
7119
|
-
|
|
7120
|
-
};
|
|
8003
|
+
pricingSource: "fallback"
|
|
8004
|
+
});
|
|
7121
8005
|
}
|
|
7122
8006
|
|
|
7123
8007
|
// src/calculator/waf.ts
|
|
@@ -7231,20 +8115,16 @@ function calculateWafCost(resource, targetProvider, targetRegion) {
|
|
|
7231
8115
|
totalMonthly += requestCost;
|
|
7232
8116
|
notes.push("GCP equivalent: Cloud Armor security policy");
|
|
7233
8117
|
}
|
|
7234
|
-
return {
|
|
7235
|
-
|
|
7236
|
-
resource_type: resource.type,
|
|
7237
|
-
resource_name: resource.name,
|
|
8118
|
+
return makeCostEstimate({
|
|
8119
|
+
resource,
|
|
7238
8120
|
provider: targetProvider,
|
|
7239
8121
|
region: targetRegion,
|
|
7240
|
-
|
|
7241
|
-
yearly_cost: totalMonthly * 12,
|
|
7242
|
-
currency: "USD",
|
|
8122
|
+
monthlyCost: totalMonthly,
|
|
7243
8123
|
breakdown,
|
|
7244
8124
|
confidence: hasExplicitRules && hasExplicitRequests ? "high" : "medium",
|
|
7245
8125
|
notes,
|
|
7246
|
-
|
|
7247
|
-
};
|
|
8126
|
+
pricingSource: "fallback"
|
|
8127
|
+
});
|
|
7248
8128
|
}
|
|
7249
8129
|
|
|
7250
8130
|
// src/calculator/search.ts
|
|
@@ -7339,20 +8219,16 @@ function calculateSearchCost(resource, targetProvider, targetRegion) {
|
|
|
7339
8219
|
"GCP has no direct managed OpenSearch service; estimate based on comparable compute cost"
|
|
7340
8220
|
);
|
|
7341
8221
|
}
|
|
7342
|
-
return {
|
|
7343
|
-
|
|
7344
|
-
resource_type: resource.type,
|
|
7345
|
-
resource_name: resource.name,
|
|
8222
|
+
return makeCostEstimate({
|
|
8223
|
+
resource,
|
|
7346
8224
|
provider: targetProvider,
|
|
7347
8225
|
region: targetRegion,
|
|
7348
|
-
|
|
7349
|
-
yearly_cost: totalMonthly * 12,
|
|
7350
|
-
currency: "USD",
|
|
8226
|
+
monthlyCost: totalMonthly,
|
|
7351
8227
|
breakdown,
|
|
7352
8228
|
confidence: targetProvider === "aws" ? "medium" : "low",
|
|
7353
8229
|
notes,
|
|
7354
|
-
|
|
7355
|
-
};
|
|
8230
|
+
pricingSource: "fallback"
|
|
8231
|
+
});
|
|
7356
8232
|
}
|
|
7357
8233
|
|
|
7358
8234
|
// src/calculator/messaging.ts
|
|
@@ -7485,20 +8361,16 @@ function calculateMessagingCost(resource, targetProvider, targetRegion) {
|
|
|
7485
8361
|
);
|
|
7486
8362
|
}
|
|
7487
8363
|
}
|
|
7488
|
-
return {
|
|
7489
|
-
|
|
7490
|
-
resource_type: resource.type,
|
|
7491
|
-
resource_name: resource.name,
|
|
8364
|
+
return makeCostEstimate({
|
|
8365
|
+
resource,
|
|
7492
8366
|
provider: targetProvider,
|
|
7493
8367
|
region: targetRegion,
|
|
7494
|
-
|
|
7495
|
-
yearly_cost: totalMonthly * 12,
|
|
7496
|
-
currency: "USD",
|
|
8368
|
+
monthlyCost: totalMonthly,
|
|
7497
8369
|
breakdown,
|
|
7498
8370
|
confidence: hasExplicitMessages ? "high" : "medium",
|
|
7499
8371
|
notes,
|
|
7500
|
-
|
|
7501
|
-
};
|
|
8372
|
+
pricingSource: "fallback"
|
|
8373
|
+
});
|
|
7502
8374
|
}
|
|
7503
8375
|
function calculateMqBrokerCost(resource, targetProvider, targetRegion) {
|
|
7504
8376
|
const notes = [];
|
|
@@ -7535,20 +8407,16 @@ function calculateMqBrokerCost(resource, targetProvider, targetRegion) {
|
|
|
7535
8407
|
unit_price: AWS_MQ_STORAGE_PER_GB,
|
|
7536
8408
|
monthly_cost: storageCost
|
|
7537
8409
|
});
|
|
7538
|
-
return {
|
|
7539
|
-
|
|
7540
|
-
resource_type: resource.type,
|
|
7541
|
-
resource_name: resource.name,
|
|
8410
|
+
return makeCostEstimate({
|
|
8411
|
+
resource,
|
|
7542
8412
|
provider: targetProvider,
|
|
7543
8413
|
region: targetRegion,
|
|
7544
|
-
|
|
7545
|
-
yearly_cost: totalMonthly * 12,
|
|
7546
|
-
currency: "USD",
|
|
8414
|
+
monthlyCost: totalMonthly,
|
|
7547
8415
|
breakdown,
|
|
7548
8416
|
confidence: hourlyPrice !== void 0 ? "medium" : "low",
|
|
7549
8417
|
notes,
|
|
7550
|
-
|
|
7551
|
-
};
|
|
8418
|
+
pricingSource: "fallback"
|
|
8419
|
+
});
|
|
7552
8420
|
}
|
|
7553
8421
|
function calculateEventHubCost(resource, targetProvider, targetRegion) {
|
|
7554
8422
|
const notes = [];
|
|
@@ -7568,20 +8436,16 @@ function calculateEventHubCost(resource, targetProvider, targetRegion) {
|
|
|
7568
8436
|
notes.push(
|
|
7569
8437
|
`Azure Event Hubs: ${sku} tier, ${capacity} ${unitLabel}(s) \u2014 $${hourlyPrice}/${unitLabel}/hr`
|
|
7570
8438
|
);
|
|
7571
|
-
return {
|
|
7572
|
-
|
|
7573
|
-
resource_type: resource.type,
|
|
7574
|
-
resource_name: resource.name,
|
|
8439
|
+
return makeCostEstimate({
|
|
8440
|
+
resource,
|
|
7575
8441
|
provider: targetProvider,
|
|
7576
8442
|
region: targetRegion,
|
|
7577
|
-
|
|
7578
|
-
yearly_cost: totalMonthly * 12,
|
|
7579
|
-
currency: "USD",
|
|
8443
|
+
monthlyCost: totalMonthly,
|
|
7580
8444
|
breakdown,
|
|
7581
8445
|
confidence: "medium",
|
|
7582
8446
|
notes,
|
|
7583
|
-
|
|
7584
|
-
};
|
|
8447
|
+
pricingSource: "fallback"
|
|
8448
|
+
});
|
|
7585
8449
|
}
|
|
7586
8450
|
function calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion) {
|
|
7587
8451
|
const notes = [];
|
|
@@ -7604,20 +8468,16 @@ function calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion)
|
|
|
7604
8468
|
notes.push(
|
|
7605
8469
|
`GCP Pub/Sub subscription: $${GCP_PUBSUB_SUBSCRIPTION_PER_MILLION_OPS}/million ack operations`
|
|
7606
8470
|
);
|
|
7607
|
-
return {
|
|
7608
|
-
|
|
7609
|
-
resource_type: resource.type,
|
|
7610
|
-
resource_name: resource.name,
|
|
8471
|
+
return makeCostEstimate({
|
|
8472
|
+
resource,
|
|
7611
8473
|
provider: targetProvider,
|
|
7612
8474
|
region: targetRegion,
|
|
7613
|
-
|
|
7614
|
-
yearly_cost: opsCost * 12,
|
|
7615
|
-
currency: "USD",
|
|
8475
|
+
monthlyCost: opsCost,
|
|
7616
8476
|
breakdown,
|
|
7617
8477
|
confidence: hasExplicitMessages ? "high" : "medium",
|
|
7618
8478
|
notes,
|
|
7619
|
-
|
|
7620
|
-
};
|
|
8479
|
+
pricingSource: "fallback"
|
|
8480
|
+
});
|
|
7621
8481
|
}
|
|
7622
8482
|
|
|
7623
8483
|
// src/calculator/ml-ai.ts
|
|
@@ -7735,20 +8595,16 @@ function calculateMlAiCost(resource, targetProvider, targetRegion) {
|
|
|
7735
8595
|
"Azure ML endpoint pricing not yet supported \u2014 consider Azure Machine Learning compute instance costs"
|
|
7736
8596
|
);
|
|
7737
8597
|
}
|
|
7738
|
-
return {
|
|
7739
|
-
|
|
7740
|
-
resource_type: resource.type,
|
|
7741
|
-
resource_name: resource.name,
|
|
8598
|
+
return makeCostEstimate({
|
|
8599
|
+
resource,
|
|
7742
8600
|
provider: targetProvider,
|
|
7743
8601
|
region: targetRegion,
|
|
7744
|
-
|
|
7745
|
-
yearly_cost: totalMonthly * 12,
|
|
7746
|
-
currency: "USD",
|
|
8602
|
+
monthlyCost: totalMonthly,
|
|
7747
8603
|
breakdown,
|
|
7748
8604
|
confidence: "low",
|
|
7749
8605
|
notes,
|
|
7750
|
-
|
|
7751
|
-
};
|
|
8606
|
+
pricingSource: "fallback"
|
|
8607
|
+
});
|
|
7752
8608
|
}
|
|
7753
8609
|
|
|
7754
8610
|
// src/calculator/cost-engine.ts
|
|
@@ -8045,20 +8901,16 @@ var CostEngine = class {
|
|
|
8045
8901
|
return calculatePubSubSubscriptionCost(resource, targetProvider, targetRegion);
|
|
8046
8902
|
}
|
|
8047
8903
|
logger.warn("CostEngine: unsupported resource type", { type, targetProvider });
|
|
8048
|
-
return {
|
|
8049
|
-
|
|
8050
|
-
resource_type: type,
|
|
8051
|
-
resource_name: resource.name,
|
|
8904
|
+
return makeCostEstimate({
|
|
8905
|
+
resource,
|
|
8052
8906
|
provider: targetProvider,
|
|
8053
8907
|
region: targetRegion,
|
|
8054
|
-
|
|
8055
|
-
yearly_cost: 0,
|
|
8056
|
-
currency: "USD",
|
|
8908
|
+
monthlyCost: 0,
|
|
8057
8909
|
breakdown: [],
|
|
8058
8910
|
confidence: "low",
|
|
8059
8911
|
notes: [`Resource type "${type}" is not yet supported by the cost engine`],
|
|
8060
|
-
|
|
8061
|
-
};
|
|
8912
|
+
pricingSource: "fallback"
|
|
8913
|
+
});
|
|
8062
8914
|
}
|
|
8063
8915
|
/**
|
|
8064
8916
|
* Calculates costs for all resources and aggregates them into a CostBreakdown.
|
|
@@ -8124,7 +8976,7 @@ var CostEngine = class {
|
|
|
8124
8976
|
name: `data-transfer-${estimate.provider}-${estimate.region}`,
|
|
8125
8977
|
provider: estimate.provider,
|
|
8126
8978
|
region: estimate.region,
|
|
8127
|
-
attributes: { monthly_egress_gb:
|
|
8979
|
+
attributes: { monthly_egress_gb: DEFAULT_EGRESS_GB },
|
|
8128
8980
|
tags: {},
|
|
8129
8981
|
source_file: "synthetic"
|
|
8130
8982
|
}
|
|
@@ -8152,6 +9004,12 @@ var CostEngine = class {
|
|
|
8152
9004
|
}
|
|
8153
9005
|
}
|
|
8154
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
|
+
}
|
|
8155
9013
|
const budgetWarnings = this.evaluateBudget(totalMonthly, estimates);
|
|
8156
9014
|
return {
|
|
8157
9015
|
provider: targetProvider,
|
|
@@ -8165,6 +9023,9 @@ var CostEngine = class {
|
|
|
8165
9023
|
by_resource: estimates,
|
|
8166
9024
|
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8167
9025
|
warnings,
|
|
9026
|
+
...estimatedEgressMonthly > 0 && {
|
|
9027
|
+
estimated_egress_monthly: Math.round(estimatedEgressMonthly * 100) / 100
|
|
9028
|
+
},
|
|
8168
9029
|
...budgetWarnings.length > 0 && { budget_warnings: budgetWarnings }
|
|
8169
9030
|
};
|
|
8170
9031
|
}
|
|
@@ -8209,84 +9070,15 @@ var CostEngine = class {
|
|
|
8209
9070
|
}
|
|
8210
9071
|
};
|
|
8211
9072
|
|
|
8212
|
-
// src/currency.ts
|
|
8213
|
-
var SUPPORTED_CURRENCIES = [
|
|
8214
|
-
"USD",
|
|
8215
|
-
"EUR",
|
|
8216
|
-
"GBP",
|
|
8217
|
-
"JPY",
|
|
8218
|
-
"CAD",
|
|
8219
|
-
"AUD",
|
|
8220
|
-
"INR",
|
|
8221
|
-
"BRL"
|
|
8222
|
-
];
|
|
8223
|
-
var EXCHANGE_RATES = {
|
|
8224
|
-
USD: 1,
|
|
8225
|
-
EUR: 0.92,
|
|
8226
|
-
GBP: 0.79,
|
|
8227
|
-
JPY: 149.5,
|
|
8228
|
-
CAD: 1.36,
|
|
8229
|
-
AUD: 1.53,
|
|
8230
|
-
INR: 83.1,
|
|
8231
|
-
BRL: 4.97
|
|
8232
|
-
};
|
|
8233
|
-
function convertCurrency(amountUsd, targetCurrency) {
|
|
8234
|
-
const rate = EXCHANGE_RATES[targetCurrency];
|
|
8235
|
-
if (rate === void 0 || rate === 1) {
|
|
8236
|
-
return amountUsd;
|
|
8237
|
-
}
|
|
8238
|
-
return amountUsd * rate;
|
|
8239
|
-
}
|
|
8240
|
-
function convertLineItem(item, currency) {
|
|
8241
|
-
return {
|
|
8242
|
-
...item,
|
|
8243
|
-
unit_price: convertCurrency(item.unit_price, currency),
|
|
8244
|
-
monthly_cost: convertCurrency(item.monthly_cost, currency)
|
|
8245
|
-
};
|
|
8246
|
-
}
|
|
8247
|
-
function convertCostEstimate(estimate, currency) {
|
|
8248
|
-
return {
|
|
8249
|
-
...estimate,
|
|
8250
|
-
monthly_cost: convertCurrency(estimate.monthly_cost, currency),
|
|
8251
|
-
yearly_cost: convertCurrency(estimate.yearly_cost, currency),
|
|
8252
|
-
currency,
|
|
8253
|
-
breakdown: estimate.breakdown.map((item) => convertLineItem(item, currency))
|
|
8254
|
-
};
|
|
8255
|
-
}
|
|
8256
|
-
function convertBreakdownCurrency(breakdown, currency) {
|
|
8257
|
-
const original_usd = {
|
|
8258
|
-
total_monthly: breakdown.total_monthly,
|
|
8259
|
-
total_yearly: breakdown.total_yearly
|
|
8260
|
-
};
|
|
8261
|
-
const convertedByService = {};
|
|
8262
|
-
for (const [service, amount] of Object.entries(breakdown.by_service)) {
|
|
8263
|
-
convertedByService[service] = convertCurrency(amount, currency);
|
|
8264
|
-
}
|
|
8265
|
-
return {
|
|
8266
|
-
...breakdown,
|
|
8267
|
-
total_monthly: convertCurrency(breakdown.total_monthly, currency),
|
|
8268
|
-
total_yearly: convertCurrency(breakdown.total_yearly, currency),
|
|
8269
|
-
currency,
|
|
8270
|
-
by_service: convertedByService,
|
|
8271
|
-
by_resource: breakdown.by_resource.map((est) => convertCostEstimate(est, currency)),
|
|
8272
|
-
original_usd
|
|
8273
|
-
};
|
|
8274
|
-
}
|
|
8275
|
-
|
|
8276
9073
|
// src/tools/estimate-cost.ts
|
|
8277
|
-
var estimateCostSchema =
|
|
8278
|
-
files:
|
|
8279
|
-
|
|
8280
|
-
|
|
8281
|
-
|
|
8282
|
-
})
|
|
8283
|
-
),
|
|
8284
|
-
tfvars: z2.string().optional().describe("Contents of terraform.tfvars file"),
|
|
8285
|
-
provider: z2.enum(["aws", "azure", "gcp"]).describe("Target cloud provider to estimate costs for"),
|
|
8286
|
-
region: z2.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(
|
|
8287
9079
|
"Target region for pricing lookup. Defaults to the source region mapped to the target provider."
|
|
8288
9080
|
),
|
|
8289
|
-
currency:
|
|
9081
|
+
currency: currencyField
|
|
8290
9082
|
});
|
|
8291
9083
|
async function estimateCost(params, pricingEngine, config) {
|
|
8292
9084
|
const inventory = await parseTerraform(params.files, params.tfvars);
|
|
@@ -8323,12 +9115,13 @@ async function estimateCost(params, pricingEngine, config) {
|
|
|
8323
9115
|
return {
|
|
8324
9116
|
...convertedRest,
|
|
8325
9117
|
by_resource: byResource,
|
|
8326
|
-
warnings: allWarnings
|
|
9118
|
+
warnings: allWarnings,
|
|
9119
|
+
pricing_metadata: getPricingMetadata(targetProvider)
|
|
8327
9120
|
};
|
|
8328
9121
|
}
|
|
8329
9122
|
|
|
8330
9123
|
// src/tools/compare-providers.ts
|
|
8331
|
-
import { z as
|
|
9124
|
+
import { z as z5 } from "zod";
|
|
8332
9125
|
|
|
8333
9126
|
// src/calculator/reserved.ts
|
|
8334
9127
|
var DISCOUNT_RATES = {
|
|
@@ -8356,8 +9149,12 @@ var DISCOUNT_RATES = {
|
|
|
8356
9149
|
{ term: "3yr", payment: "partial_upfront", rate: 0.45 }
|
|
8357
9150
|
]
|
|
8358
9151
|
};
|
|
8359
|
-
function calculateReservedPricing(onDemandMonthly, provider) {
|
|
8360
|
-
const
|
|
9152
|
+
function calculateReservedPricing(onDemandMonthly, provider, overrides) {
|
|
9153
|
+
const baseRates = DISCOUNT_RATES[provider];
|
|
9154
|
+
const rates = overrides ? baseRates.map((r) => {
|
|
9155
|
+
const override = overrides[r.term];
|
|
9156
|
+
return override !== void 0 ? { ...r, rate: override } : r;
|
|
9157
|
+
}) : baseRates;
|
|
8361
9158
|
const options = rates.map(({ term, payment, rate }) => {
|
|
8362
9159
|
const monthly_cost = onDemandMonthly * (1 - rate);
|
|
8363
9160
|
const monthly_savings = onDemandMonthly - monthly_cost;
|
|
@@ -8872,15 +9669,23 @@ function generateJsonReport(comparison, resources, monthlyHours = 730, parseWarn
|
|
|
8872
9669
|
return JSON.stringify(output);
|
|
8873
9670
|
}
|
|
8874
9671
|
|
|
8875
|
-
// src/reporting/csv-
|
|
8876
|
-
var PROVIDERS = ["aws", "azure", "gcp"];
|
|
9672
|
+
// src/reporting/csv-escape.ts
|
|
8877
9673
|
function csvEscape(value) {
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
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, '""')}"`;
|
|
8881
9683
|
}
|
|
8882
9684
|
return str2;
|
|
8883
9685
|
}
|
|
9686
|
+
|
|
9687
|
+
// src/reporting/csv-report.ts
|
|
9688
|
+
var PROVIDERS = ["aws", "azure", "gcp"];
|
|
8884
9689
|
function generateCsvReport(comparison, resources, options = {}) {
|
|
8885
9690
|
const lines = [];
|
|
8886
9691
|
const tagKey = options.group_by === "tag" ? options.group_by_tag_key : void 0;
|
|
@@ -9041,15 +9846,8 @@ function billingPeriodEnd() {
|
|
|
9041
9846
|
const now = /* @__PURE__ */ new Date();
|
|
9042
9847
|
return new Date(now.getFullYear(), now.getMonth() + 1, 1).toISOString().split("T")[0];
|
|
9043
9848
|
}
|
|
9044
|
-
function csvEscape2(value) {
|
|
9045
|
-
const str2 = String(value);
|
|
9046
|
-
if (str2.includes(",") || str2.includes("\n") || str2.includes('"')) {
|
|
9047
|
-
return `"${str2.replace(/"/g, '""')}"`;
|
|
9048
|
-
}
|
|
9049
|
-
return str2;
|
|
9050
|
-
}
|
|
9051
9849
|
function buildRow(values) {
|
|
9052
|
-
return values.map(
|
|
9850
|
+
return values.map(csvEscape).join(",");
|
|
9053
9851
|
}
|
|
9054
9852
|
function generateFocusReport(comparison, resources) {
|
|
9055
9853
|
const lines = [];
|
|
@@ -9116,17 +9914,12 @@ function generateFocusReport(comparison, resources) {
|
|
|
9116
9914
|
}
|
|
9117
9915
|
|
|
9118
9916
|
// src/tools/compare-providers.ts
|
|
9119
|
-
var compareProvidersSchema =
|
|
9120
|
-
files:
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
),
|
|
9126
|
-
tfvars: z3.string().optional().describe("Contents of terraform.tfvars file"),
|
|
9127
|
-
format: z3.enum(["markdown", "json", "csv", "focus"]).default("markdown").describe("Output report format"),
|
|
9128
|
-
providers: z3.array(z3.enum(["aws", "azure", "gcp"])).default(["aws", "azure", "gcp"]).describe("Cloud providers to include in the comparison"),
|
|
9129
|
-
currency: z3.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
|
|
9130
9923
|
});
|
|
9131
9924
|
async function compareProviders(params, pricingEngine, config) {
|
|
9132
9925
|
const inventory = await parseTerraform(params.files, params.tfvars);
|
|
@@ -9182,11 +9975,13 @@ async function compareProviders(params, pricingEngine, config) {
|
|
|
9182
9975
|
break;
|
|
9183
9976
|
}
|
|
9184
9977
|
const dedupedWarnings = [.../* @__PURE__ */ new Set([...parseWarnings, ...allWarnings])];
|
|
9978
|
+
const pricingMetadata = Object.fromEntries(providerList.map((p) => [p, getPricingMetadata(p)]));
|
|
9185
9979
|
if (params.format === "json") {
|
|
9186
9980
|
return {
|
|
9187
9981
|
report,
|
|
9188
9982
|
format: params.format,
|
|
9189
|
-
warnings: dedupedWarnings
|
|
9983
|
+
warnings: dedupedWarnings,
|
|
9984
|
+
pricing_metadata: pricingMetadata
|
|
9190
9985
|
};
|
|
9191
9986
|
}
|
|
9192
9987
|
const comparisonSummary = {
|
|
@@ -9203,21 +9998,17 @@ async function compareProviders(params, pricingEngine, config) {
|
|
|
9203
9998
|
report,
|
|
9204
9999
|
format: params.format,
|
|
9205
10000
|
comparison: comparisonSummary,
|
|
9206
|
-
warnings: dedupedWarnings
|
|
10001
|
+
warnings: dedupedWarnings,
|
|
10002
|
+
pricing_metadata: pricingMetadata
|
|
9207
10003
|
};
|
|
9208
10004
|
}
|
|
9209
10005
|
|
|
9210
10006
|
// src/tools/optimize-cost.ts
|
|
9211
|
-
import { z as
|
|
9212
|
-
var optimizeCostSchema =
|
|
9213
|
-
files:
|
|
9214
|
-
|
|
9215
|
-
|
|
9216
|
-
content: z4.string().describe("File content (HCL)")
|
|
9217
|
-
})
|
|
9218
|
-
),
|
|
9219
|
-
tfvars: z4.string().optional().describe("Contents of terraform.tfvars file"),
|
|
9220
|
-
providers: z4.array(z4.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")
|
|
9221
10012
|
});
|
|
9222
10013
|
async function optimizeCost(params, pricingEngine, config) {
|
|
9223
10014
|
const inventory = await parseTerraform(params.files, params.tfvars);
|
|
@@ -9253,24 +10044,23 @@ async function optimizeCost(params, pricingEngine, config) {
|
|
|
9253
10044
|
}
|
|
9254
10045
|
|
|
9255
10046
|
// src/tools/what-if.ts
|
|
9256
|
-
import { z as
|
|
9257
|
-
var whatIfSchema =
|
|
9258
|
-
files:
|
|
9259
|
-
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
9263
|
-
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
new_value: z5.union([z5.string(), z5.number()]).describe("The new value for the attribute")
|
|
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({
|
|
10052
|
+
resource_id: shortStringSchema.describe(
|
|
10053
|
+
"The resource ID to modify (e.g. aws_instance.web)"
|
|
10054
|
+
),
|
|
10055
|
+
attribute: shortStringSchema.describe(
|
|
10056
|
+
"The attribute name to change (e.g. instance_type, storage_size_gb)"
|
|
10057
|
+
),
|
|
10058
|
+
new_value: z7.union([shortStringSchema, z7.number()]).describe("The new value for the attribute")
|
|
9269
10059
|
})
|
|
9270
|
-
).describe("List of attribute changes to simulate"),
|
|
9271
|
-
provider:
|
|
9272
|
-
region:
|
|
9273
|
-
tfvars:
|
|
10060
|
+
).max(200, "changes array exceeds 200 entries").describe("List of attribute changes to simulate"),
|
|
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")
|
|
9274
10064
|
});
|
|
9275
10065
|
function cloneResources(resources) {
|
|
9276
10066
|
return JSON.parse(JSON.stringify(resources));
|
|
@@ -9366,23 +10156,34 @@ async function whatIf(params, pricingEngine, config) {
|
|
|
9366
10156
|
}
|
|
9367
10157
|
|
|
9368
10158
|
export {
|
|
9369
|
-
loadConfig,
|
|
9370
10159
|
setLogLevel,
|
|
9371
10160
|
logger,
|
|
10161
|
+
loadConfig,
|
|
10162
|
+
SUPPORTED_CURRENCIES,
|
|
10163
|
+
convertCurrency,
|
|
10164
|
+
getExchangeRateInfo,
|
|
10165
|
+
convertBreakdownCurrency,
|
|
9372
10166
|
PricingCache,
|
|
9373
10167
|
PricingEngine,
|
|
10168
|
+
sanitizeForMessage,
|
|
10169
|
+
coerceToString,
|
|
9374
10170
|
str,
|
|
9375
10171
|
num,
|
|
9376
10172
|
bool,
|
|
10173
|
+
safeJsonParse,
|
|
9377
10174
|
parseTerraform,
|
|
10175
|
+
planJsonSchema,
|
|
10176
|
+
stateJsonSchema,
|
|
10177
|
+
shortStringSchema,
|
|
10178
|
+
providerEnum,
|
|
10179
|
+
iacFilesSchema,
|
|
10180
|
+
tfvarsField,
|
|
10181
|
+
currencyField,
|
|
9378
10182
|
analyzeTerraformSchema,
|
|
9379
10183
|
analyzeTerraform,
|
|
9380
10184
|
mapRegion,
|
|
9381
10185
|
mapInstance,
|
|
9382
10186
|
CostEngine,
|
|
9383
|
-
SUPPORTED_CURRENCIES,
|
|
9384
|
-
convertCurrency,
|
|
9385
|
-
convertBreakdownCurrency,
|
|
9386
10187
|
estimateCostSchema,
|
|
9387
10188
|
estimateCost,
|
|
9388
10189
|
compareProvidersSchema,
|
|
@@ -9392,4 +10193,4 @@ export {
|
|
|
9392
10193
|
whatIfSchema,
|
|
9393
10194
|
whatIf
|
|
9394
10195
|
};
|
|
9395
|
-
//# sourceMappingURL=chunk-
|
|
10196
|
+
//# sourceMappingURL=chunk-CGMU4ETN.js.map
|