@pagopa/dx-savemoney 0.3.2 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/__tests__/finding.test.js +22 -0
- package/dist/__tests__/finding.test.js.map +1 -1
- package/dist/azure/__tests__/analyzer-tags.test.js +28 -1
- package/dist/azure/__tests__/analyzer-tags.test.js.map +1 -1
- package/dist/azure/__tests__/config.test.js +10 -0
- package/dist/azure/__tests__/config.test.js.map +1 -1
- package/dist/azure/__tests__/report.test.js +71 -0
- package/dist/azure/__tests__/report.test.js.map +1 -1
- package/dist/azure/analyzer.d.ts +8 -1
- package/dist/azure/analyzer.d.ts.map +1 -1
- package/dist/azure/analyzer.js +55 -37
- package/dist/azure/analyzer.js.map +1 -1
- package/dist/azure/analyzers/registry.d.ts.map +1 -1
- package/dist/azure/analyzers/registry.js +4 -4
- package/dist/azure/analyzers/registry.js.map +1 -1
- package/dist/azure/analyzers/types.d.ts +6 -0
- package/dist/azure/analyzers/types.d.ts.map +1 -1
- package/dist/azure/pricing/__tests__/cache.test.d.ts +8 -0
- package/dist/azure/pricing/__tests__/cache.test.d.ts.map +1 -0
- package/dist/azure/pricing/__tests__/cache.test.js +56 -0
- package/dist/azure/pricing/__tests__/cache.test.js.map +1 -0
- package/dist/azure/pricing/__tests__/client.test.d.ts +9 -0
- package/dist/azure/pricing/__tests__/client.test.d.ts.map +1 -0
- package/dist/azure/pricing/__tests__/client.test.js +181 -0
- package/dist/azure/pricing/__tests__/client.test.js.map +1 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.d.ts +6 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.d.ts.map +1 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.js +283 -0
- package/dist/azure/pricing/__tests__/pricing-service.test.js.map +1 -0
- package/dist/azure/pricing/cache.d.ts +45 -0
- package/dist/azure/pricing/cache.d.ts.map +1 -0
- package/dist/azure/pricing/cache.js +79 -0
- package/dist/azure/pricing/cache.js.map +1 -0
- package/dist/azure/pricing/client.d.ts +67 -0
- package/dist/azure/pricing/client.d.ts.map +1 -0
- package/dist/azure/pricing/client.js +96 -0
- package/dist/azure/pricing/client.js.map +1 -0
- package/dist/azure/pricing/index.d.ts +6 -0
- package/dist/azure/pricing/index.d.ts.map +1 -0
- package/dist/azure/pricing/index.js +6 -0
- package/dist/azure/pricing/index.js.map +1 -0
- package/dist/azure/pricing/pricing-service.d.ts +46 -0
- package/dist/azure/pricing/pricing-service.d.ts.map +1 -0
- package/dist/azure/pricing/pricing-service.js +84 -0
- package/dist/azure/pricing/pricing-service.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.d.ts +5 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.js +137 -0
- package/dist/azure/pricing/resolvers/__tests__/app-service.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.d.ts +5 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.js +160 -0
- package/dist/azure/pricing/resolvers/__tests__/disk.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.d.ts +5 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.js +113 -0
- package/dist/azure/pricing/resolvers/__tests__/public-ip.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.d.ts +9 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.js +204 -0
- package/dist/azure/pricing/resolvers/__tests__/vm.test.js.map +1 -0
- package/dist/azure/pricing/resolvers/app-service.d.ts +27 -0
- package/dist/azure/pricing/resolvers/app-service.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/app-service.js +67 -0
- package/dist/azure/pricing/resolvers/app-service.js.map +1 -0
- package/dist/azure/pricing/resolvers/disk.d.ts +59 -0
- package/dist/azure/pricing/resolvers/disk.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/disk.js +140 -0
- package/dist/azure/pricing/resolvers/disk.js.map +1 -0
- package/dist/azure/pricing/resolvers/index.d.ts +12 -0
- package/dist/azure/pricing/resolvers/index.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/index.js +8 -0
- package/dist/azure/pricing/resolvers/index.js.map +1 -0
- package/dist/azure/pricing/resolvers/public-ip.d.ts +32 -0
- package/dist/azure/pricing/resolvers/public-ip.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/public-ip.js +56 -0
- package/dist/azure/pricing/resolvers/public-ip.js.map +1 -0
- package/dist/azure/pricing/resolvers/vm.d.ts +51 -0
- package/dist/azure/pricing/resolvers/vm.d.ts.map +1 -0
- package/dist/azure/pricing/resolvers/vm.js +89 -0
- package/dist/azure/pricing/resolvers/vm.js.map +1 -0
- package/dist/azure/pricing/schema.d.ts +88 -0
- package/dist/azure/pricing/schema.d.ts.map +1 -0
- package/dist/azure/pricing/schema.js +64 -0
- package/dist/azure/pricing/schema.js.map +1 -0
- package/dist/azure/pricing/test-cache-dir.d.ts +3 -0
- package/dist/azure/pricing/test-cache-dir.d.ts.map +1 -0
- package/dist/azure/pricing/test-cache-dir.js +17 -0
- package/dist/azure/pricing/test-cache-dir.js.map +1 -0
- package/dist/azure/report.d.ts.map +1 -1
- package/dist/azure/report.js +57 -18
- package/dist/azure/report.js.map +1 -1
- package/dist/azure/resources/__tests__/app-service.test.d.ts +5 -0
- package/dist/azure/resources/__tests__/app-service.test.d.ts.map +1 -0
- package/dist/azure/resources/__tests__/app-service.test.js +105 -0
- package/dist/azure/resources/__tests__/app-service.test.js.map +1 -0
- package/dist/azure/resources/app-service.d.ts +8 -3
- package/dist/azure/resources/app-service.d.ts.map +1 -1
- package/dist/azure/resources/app-service.js +35 -2
- package/dist/azure/resources/app-service.js.map +1 -1
- package/dist/azure/resources/disk.d.ts +3 -1
- package/dist/azure/resources/disk.d.ts.map +1 -1
- package/dist/azure/resources/disk.js +35 -3
- package/dist/azure/resources/disk.js.map +1 -1
- package/dist/azure/resources/public-ip.d.ts +3 -1
- package/dist/azure/resources/public-ip.d.ts.map +1 -1
- package/dist/azure/resources/public-ip.js +41 -3
- package/dist/azure/resources/public-ip.js.map +1 -1
- package/dist/azure/resources/vm.d.ts +4 -1
- package/dist/azure/resources/vm.d.ts.map +1 -1
- package/dist/azure/resources/vm.js +49 -6
- package/dist/azure/resources/vm.js.map +1 -1
- package/dist/azure/types.d.ts +13 -0
- package/dist/azure/types.d.ts.map +1 -1
- package/dist/finding.d.ts +3 -3
- package/dist/finding.d.ts.map +1 -1
- package/dist/finding.js +7 -0
- package/dist/finding.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +8 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -1
- package/package.json +9 -9
- package/src/__tests__/finding.test.ts +24 -0
- package/src/azure/__tests__/analyzer-tags.test.ts +36 -1
- package/src/azure/__tests__/config.test.ts +12 -0
- package/src/azure/__tests__/report.test.ts +78 -0
- package/src/azure/analyzer.ts +67 -43
- package/src/azure/analyzers/registry.ts +8 -2
- package/src/azure/analyzers/types.ts +6 -0
- package/src/azure/pricing/__tests__/cache.test.ts +67 -0
- package/src/azure/pricing/__tests__/client.test.ts +212 -0
- package/src/azure/pricing/__tests__/fixtures/vm-b2s-westeurope.json +51 -0
- package/src/azure/pricing/__tests__/pricing-service.test.ts +315 -0
- package/src/azure/pricing/cache.ts +92 -0
- package/src/azure/pricing/client.ts +137 -0
- package/src/azure/pricing/index.ts +6 -0
- package/src/azure/pricing/pricing-service.ts +125 -0
- package/src/azure/pricing/resolvers/__tests__/app-service.test.ts +154 -0
- package/src/azure/pricing/resolvers/__tests__/disk.test.ts +189 -0
- package/src/azure/pricing/resolvers/__tests__/public-ip.test.ts +130 -0
- package/src/azure/pricing/resolvers/__tests__/vm.test.ts +232 -0
- package/src/azure/pricing/resolvers/app-service.ts +102 -0
- package/src/azure/pricing/resolvers/disk.ts +170 -0
- package/src/azure/pricing/resolvers/index.ts +31 -0
- package/src/azure/pricing/resolvers/public-ip.ts +87 -0
- package/src/azure/pricing/resolvers/vm.ts +125 -0
- package/src/azure/pricing/schema.ts +69 -0
- package/src/azure/pricing/test-cache-dir.ts +26 -0
- package/src/azure/report.ts +86 -21
- package/src/azure/resources/__tests__/app-service.test.ts +165 -0
- package/src/azure/resources/app-service.ts +65 -4
- package/src/azure/resources/disk.ts +46 -4
- package/src/azure/resources/public-ip.ts +61 -2
- package/src/azure/resources/vm.ts +89 -11
- package/src/azure/types.ts +14 -0
- package/src/finding.ts +10 -3
- package/src/index.ts +3 -3
- package/src/types.ts +12 -0
|
@@ -11,9 +11,10 @@ import { getMetric, verboseLog, verboseLogAnalysisResult, verboseLogResourceStar
|
|
|
11
11
|
* @param networkClient - Azure Network client for Public IP details
|
|
12
12
|
* @param monitorClient - Azure Monitor client for metrics
|
|
13
13
|
* @param timespanDays - Number of days to analyze metrics
|
|
14
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
14
15
|
* @returns Analysis result with cost risk and reason
|
|
15
16
|
*/
|
|
16
|
-
export async function analyzePublicIp(resource, networkClient, monitorClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache) {
|
|
17
|
+
export async function analyzePublicIp(resource, networkClient, monitorClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache, pricing) {
|
|
17
18
|
verboseLogResourceStart(verbose, resource.name || "unknown", "Public IP (microsoft.network/publicipaddresses)");
|
|
18
19
|
verboseLog(verbose, "Resource details:", resource);
|
|
19
20
|
const costRisk = "medium";
|
|
@@ -29,9 +30,12 @@ export async function analyzePublicIp(resource, networkClient, monitorClient, ti
|
|
|
29
30
|
const resourceParts = resource.id.split("/");
|
|
30
31
|
const resourceGroupName = resourceParts[4];
|
|
31
32
|
const publicIpName = resourceParts[8];
|
|
33
|
+
// Capture details outside the try so the pricing enrichment below can
|
|
34
|
+
// reuse them without an extra API round-trip.
|
|
35
|
+
let publicIpDetails;
|
|
32
36
|
try {
|
|
33
37
|
// Get detailed Public IP information
|
|
34
|
-
|
|
38
|
+
publicIpDetails = await networkClient.publicIPAddresses.get(resourceGroupName, publicIpName);
|
|
35
39
|
verboseLog(verbose, "Public IP API details:", publicIpDetails);
|
|
36
40
|
// Check if Public IP is not associated with any resource
|
|
37
41
|
if (!publicIpDetails.ipConfiguration && !publicIpDetails.natGateway) {
|
|
@@ -54,8 +58,42 @@ export async function analyzePublicIp(resource, networkClient, monitorClient, ti
|
|
|
54
58
|
reason += "Could not retrieve detailed Public IP information. ";
|
|
55
59
|
}
|
|
56
60
|
const suspectedUnused = reason.length > 0;
|
|
57
|
-
const
|
|
61
|
+
const baseResult = { costRisk, reason: reason.trim(), suspectedUnused };
|
|
62
|
+
const result = await enrichWithPricing(baseResult, resource, publicIpDetails, pricing);
|
|
58
63
|
verboseLogAnalysisResult(verbose, result);
|
|
59
64
|
return result;
|
|
60
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Best-effort enrichment of a Public IP analysis result with the
|
|
68
|
+
* estimated monthly cost recoverable by removing the resource.
|
|
69
|
+
*/
|
|
70
|
+
async function enrichWithPricing(result, resource, publicIpDetails, pricing) {
|
|
71
|
+
if (!pricing || !result.suspectedUnused || !publicIpDetails) {
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const skuName = publicIpDetails.sku?.name?.toLowerCase();
|
|
76
|
+
const allocation = publicIpDetails.publicIPAllocationMethod?.toLowerCase();
|
|
77
|
+
const armRegionName = publicIpDetails.location ?? resource.location;
|
|
78
|
+
if (!armRegionName ||
|
|
79
|
+
(skuName !== "basic" && skuName !== "standard") ||
|
|
80
|
+
(allocation !== "static" && allocation !== "dynamic")) {
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
const estimatedMonthlySavings = await pricing.resolvePublicIp({
|
|
84
|
+
allocation,
|
|
85
|
+
armRegionName,
|
|
86
|
+
sku: skuName,
|
|
87
|
+
});
|
|
88
|
+
if (!estimatedMonthlySavings) {
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
return { ...result, estimatedMonthlySavings };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
95
|
+
logger.warn(`Failed to enrich Public IP with pricing: ${error instanceof Error ? error.message : error}`);
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
61
99
|
//# sourceMappingURL=public-ip.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"public-ip.js","sourceRoot":"","sources":["../../../src/azure/resources/public-ip.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"public-ip.js","sourceRoot":"","sources":["../../../src/azure/resources/public-ip.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,6 +5,7 @@ import type { ComputeManagementClient } from "@azure/arm-compute";
|
|
|
5
5
|
import type { MonitorClient } from "@azure/arm-monitor";
|
|
6
6
|
import * as armResources from "@azure/arm-resources";
|
|
7
7
|
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
8
|
+
import type { PricingService } from "../pricing/pricing-service.js";
|
|
8
9
|
import { type MetricsCache } from "../utils.js";
|
|
9
10
|
/**
|
|
10
11
|
* Analyzes an Azure Virtual Machine for potential cost optimization.
|
|
@@ -14,7 +15,9 @@ import { type MetricsCache } from "../utils.js";
|
|
|
14
15
|
* @param computeClient - Azure Compute client for VM details
|
|
15
16
|
* @param timespanDays - Number of days to analyze metrics
|
|
16
17
|
* @param verbose - Whether verbose logging is enabled
|
|
18
|
+
* @param cache - Run-scoped metrics cache
|
|
19
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
17
20
|
* @returns Analysis result with cost risk and reason
|
|
18
21
|
*/
|
|
19
|
-
export declare function analyzeVM(resource: armResources.GenericResource, monitorClient: MonitorClient, computeClient: ComputeManagementClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache): Promise<AnalysisResult>;
|
|
22
|
+
export declare function analyzeVM(resource: armResources.GenericResource, monitorClient: MonitorClient, computeClient: ComputeManagementClient, timespanDays: number, thresholds?: Thresholds, verbose?: boolean, cache?: MetricsCache, pricing?: PricingService): Promise<AnalysisResult>;
|
|
20
23
|
//# sourceMappingURL=vm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"vm.d.ts","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,OAAO,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAGrD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAGpE,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;GAWG;AACH,wBAAsB,SAAS,CAC7B,QAAQ,EAAE,YAAY,CAAC,eAAe,EACtC,aAAa,EAAE,aAAa,EAC5B,aAAa,EAAE,uBAAuB,EACtC,YAAY,EAAE,MAAM,EACpB,UAAU,GAAE,UAA+B,EAC3C,OAAO,UAAQ,EACf,KAAK,CAAC,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,cAAc,CAAC,CAkHzB"}
|
|
@@ -12,9 +12,11 @@ import { getMetric, verboseLog, verboseLogAnalysisResult, verboseLogResourceStar
|
|
|
12
12
|
* @param computeClient - Azure Compute client for VM details
|
|
13
13
|
* @param timespanDays - Number of days to analyze metrics
|
|
14
14
|
* @param verbose - Whether verbose logging is enabled
|
|
15
|
+
* @param cache - Run-scoped metrics cache
|
|
16
|
+
* @param pricing - Optional pricing facade for estimated monthly cost
|
|
15
17
|
* @returns Analysis result with cost risk and reason
|
|
16
18
|
*/
|
|
17
|
-
export async function analyzeVM(resource, monitorClient, computeClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache) {
|
|
19
|
+
export async function analyzeVM(resource, monitorClient, computeClient, timespanDays, thresholds = DEFAULT_THRESHOLDS, verbose = false, cache, pricing) {
|
|
18
20
|
verboseLogResourceStart(verbose, resource.name || "unknown", "Virtual Machine (microsoft.compute/virtualmachines)");
|
|
19
21
|
verboseLog(verbose, "Resource details:", resource);
|
|
20
22
|
const costRisk = "high";
|
|
@@ -37,20 +39,20 @@ export async function analyzeVM(resource, monitorClient, computeClient, timespan
|
|
|
37
39
|
// Check power state from instance view
|
|
38
40
|
const vmStatus = instanceView.statuses?.find((s) => s.code?.startsWith("PowerState/"));
|
|
39
41
|
if (vmStatus?.code === "PowerState/deallocated") {
|
|
40
|
-
const result = {
|
|
42
|
+
const result = await enrichWithPricing({
|
|
41
43
|
costRisk,
|
|
42
44
|
reason: "VM is deallocated. ",
|
|
43
45
|
suspectedUnused: true,
|
|
44
|
-
};
|
|
46
|
+
}, resource, computeClient, resourceGroupName, vmName, pricing);
|
|
45
47
|
verboseLogAnalysisResult(verbose, result);
|
|
46
48
|
return result;
|
|
47
49
|
}
|
|
48
50
|
if (vmStatus?.code === "PowerState/stopped") {
|
|
49
|
-
const result = {
|
|
51
|
+
const result = await enrichWithPricing({
|
|
50
52
|
costRisk,
|
|
51
53
|
reason: "VM is stopped. ",
|
|
52
54
|
suspectedUnused: true,
|
|
53
|
-
};
|
|
55
|
+
}, resource, computeClient, resourceGroupName, vmName, pricing);
|
|
54
56
|
verboseLogAnalysisResult(verbose, result);
|
|
55
57
|
return result;
|
|
56
58
|
}
|
|
@@ -69,8 +71,49 @@ export async function analyzeVM(resource, monitorClient, computeClient, timespan
|
|
|
69
71
|
if (networkIn !== null && networkIn < thresholds.vm.networkInBytesPerDay) {
|
|
70
72
|
reason += `Low network traffic (${(networkIn / 1024 / 1024).toFixed(2)} MB/day avg). `;
|
|
71
73
|
}
|
|
72
|
-
const result = { costRisk, reason, suspectedUnused: reason.length > 0 };
|
|
74
|
+
const result = await enrichWithPricing({ costRisk, reason, suspectedUnused: reason.length > 0 }, resource, computeClient, resourceGroupName, vmName, pricing);
|
|
73
75
|
verboseLogAnalysisResult(verbose, result);
|
|
74
76
|
return result;
|
|
75
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Best-effort enrichment of a VM analysis result with the estimated
|
|
80
|
+
* monthly cost recoverable by removing/right-sizing the resource.
|
|
81
|
+
*
|
|
82
|
+
* No-op when:
|
|
83
|
+
* - the result is not flagged as suspected unused
|
|
84
|
+
* - no pricing facade was provided
|
|
85
|
+
* - the VM SKU or region cannot be determined
|
|
86
|
+
* - the Retail Prices lookup returns no matching meter
|
|
87
|
+
*
|
|
88
|
+
* Any error along the way is logged at warn level and swallowed so the
|
|
89
|
+
* main analysis never fails because of a pricing problem.
|
|
90
|
+
*/
|
|
91
|
+
async function enrichWithPricing(result, resource, computeClient, resourceGroupName, vmName, pricing) {
|
|
92
|
+
if (!pricing || !result.suspectedUnused) {
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const details = await computeClient.virtualMachines.get(resourceGroupName, vmName);
|
|
97
|
+
const armSkuName = details.hardwareProfile?.vmSize;
|
|
98
|
+
const armRegionName = details.location ?? resource.location;
|
|
99
|
+
if (!armSkuName || !armRegionName) {
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
const os = details.storageProfile?.osDisk?.osType?.toLowerCase();
|
|
103
|
+
const estimatedMonthlySavings = await pricing.resolveVm({
|
|
104
|
+
armRegionName,
|
|
105
|
+
armSkuName,
|
|
106
|
+
os: os === "windows" ? "windows" : "linux",
|
|
107
|
+
});
|
|
108
|
+
if (!estimatedMonthlySavings) {
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
return { ...result, estimatedMonthlySavings };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
const logger = getLogger(["savemoney", "azure"]);
|
|
115
|
+
logger.warn(`Failed to enrich VM ${vmName} with pricing: ${error instanceof Error ? error.message : error}`);
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
76
119
|
//# sourceMappingURL=vm.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vm.js","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"vm.js","sourceRoot":"","sources":["../../../src/azure/resources/vm.ts"],"names":[],"mappings":"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"}
|
package/dist/azure/types.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export type AzureConfig = BaseConfig & {
|
|
|
19
19
|
* If omitted, all resources are analyzed.
|
|
20
20
|
*/
|
|
21
21
|
filterTags?: Map<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* Runtime-only pricing enrichment switch. This is intentionally not exposed
|
|
24
|
+
* in YAML config: the Retail Prices lookup uses internal defaults unless the
|
|
25
|
+
* CLI disables it for offline/debug runs.
|
|
26
|
+
*/
|
|
27
|
+
pricing?: PricingConfig;
|
|
22
28
|
/**
|
|
23
29
|
* Which finding sources to include in the run.
|
|
24
30
|
* - `"custom"` → enables the per-resource analyzer plugins
|
|
@@ -72,4 +78,11 @@ export type AzureResourceReport = {
|
|
|
72
78
|
* filter for Azure runs and would silently produce an empty report.
|
|
73
79
|
*/
|
|
74
80
|
export type AzureSource = "advisor" | "custom";
|
|
81
|
+
export type PricingConfig = {
|
|
82
|
+
/**
|
|
83
|
+
* Runtime switch used by the CLI `--no-pricing` flag. Pricing otherwise
|
|
84
|
+
* stays enabled with internal defaults for currency and cache settings.
|
|
85
|
+
*/
|
|
86
|
+
enabled?: boolean;
|
|
87
|
+
};
|
|
75
88
|
//# sourceMappingURL=types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/azure/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,QAAQ,EACR,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,EAAE,YAAY,CAAC,eAAe,CAAC;CACxC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/azure/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,KAAK,YAAY,MAAM,sBAAsB,CAAC;AAE1D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EACV,cAAc,EACd,UAAU,EACV,QAAQ,EACR,UAAU,EACX,MAAM,aAAa,CAAC;AAErB;;GAEG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG;IACrC;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,EAAE,CAAC;IACxB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B;;OAEG;IACH,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,EAAE,cAAc,CAAC;IACzB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;IACrB,QAAQ,EAAE,YAAY,CAAC,eAAe,CAAC;CACxC,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE/C,MAAM,MAAM,aAAa,GAAG;IAC1B;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC"}
|
package/dist/finding.d.ts
CHANGED
|
@@ -30,9 +30,9 @@ export type Finding = {
|
|
|
30
30
|
*/
|
|
31
31
|
code: string;
|
|
32
32
|
/**
|
|
33
|
-
* Estimated monthly
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Estimated monthly saving for this specific finding. Populated by Advisor
|
|
34
|
+
* recommendations; custom Retail Prices estimates stay at resource level
|
|
35
|
+
* because they represent cost at risk, not a per-finding saving.
|
|
36
36
|
*/
|
|
37
37
|
estimatedMonthlySavings?: Money;
|
|
38
38
|
/**
|
package/dist/finding.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB;;;OAGG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,KAAK,CAAC;IAChC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,EAAE,QAAQ,CAAC;IACnB;;OAEG;IACH,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,uBAAuB,GACvB,aAAa,GACb,aAAa,GACb,UAAU,CAAC;AAEf;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,SAAS,GAAG,OAAO,IAAI;IAChD,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"finding.d.ts","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,MAAM,OAAO,GAAG;IACpB;;;OAGG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,uBAAuB,CAAC,EAAE,KAAK,CAAC;IAChC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;;OAGG;IACH,QAAQ,EAAE,QAAQ,CAAC;IACnB;;OAEG;IACH,MAAM,EAAE,aAAa,CAAC;CACvB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,eAAe,GACvB,MAAM,GACN,uBAAuB,GACvB,aAAa,GACb,aAAa,GACb,UAAU,CAAC;AAEf;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEzD;;;GAGG;AACH,MAAM,MAAM,KAAK,GAAG;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,cAAc,CAAC,SAAS,GAAG,OAAO,IAAI;IAChD,QAAQ,EAAE,OAAO,EAAE,CAAC;IACpB,QAAQ,EAAE,SAAS,CAAC;CACrB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,EAAE;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,QAAQ,CAAC;IACnB,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB,GAAG,OAAO,EAAE,CAqBZ"}
|
package/dist/finding.js
CHANGED
|
@@ -27,6 +27,13 @@ export function findingsFromAnalysisResult(args) {
|
|
|
27
27
|
if (sentences.length === 0) {
|
|
28
28
|
return [];
|
|
29
29
|
}
|
|
30
|
+
// We intentionally do NOT propagate any monetary estimate down to the
|
|
31
|
+
// individual findings: a per-resource cost (e.g. €29.94/mo for a VM)
|
|
32
|
+
// attached to a sentence like "No tags found" would falsely suggest
|
|
33
|
+
// "add the missing tags to save €29.94". The resource-level cost is
|
|
34
|
+
// carried instead by `AnalysisResult.estimatedMonthlySavings` and
|
|
35
|
+
// surfaced by the report layer as a per-resource header — see
|
|
36
|
+
// `azure/report.ts`.
|
|
30
37
|
return sentences.map((sentence) => ({
|
|
31
38
|
category: "cost",
|
|
32
39
|
code: code ?? "custom.unknown",
|
package/dist/finding.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgGH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,0BAA0B,CAAC,IAM1C;IACC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC;IACvE,MAAM,SAAS,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClC,QAAQ,EAAE,MAAe;QACzB,IAAI,EAAE,IAAI,IAAI,gBAAgB;QAC9B,MAAM,EAAE,QAAQ;QAChB,UAAU;QACV,QAAQ;QACR,MAAM;KACP,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,MAAc;IAC9C,OAAO,MAAM;SACV,KAAK,CAAC,WAAW,CAAC;SAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC"}
|
|
1
|
+
{"version":3,"file":"finding.js","sourceRoot":"","sources":["../src/finding.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAgGH;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,0BAA0B,CAAC,IAM1C;IACC,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,QAAQ,EAAE,GAAG,IAAI,CAAC;IACvE,MAAM,SAAS,GAAG,wBAAwB,CAAC,MAAM,CAAC,CAAC;IACnD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC3B,OAAO,EAAE,CAAC;IACZ,CAAC;IACD,sEAAsE;IACtE,qEAAqE;IACrE,oEAAoE;IACpE,oEAAoE;IACpE,kEAAkE;IAClE,8DAA8D;IAC9D,qBAAqB;IACrB,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAClC,QAAQ,EAAE,MAAe;QACzB,IAAI,EAAE,IAAI,IAAI,gBAAgB;QAC9B,MAAM,EAAE,QAAQ;QAChB,UAAU;QACV,QAAQ;QACR,MAAM;KACP,CAAC,CAAC,CAAC;AACN,CAAC;AAED;;;;GAIG;AACH,SAAS,wBAAwB,CAAC,MAAc;IAC9C,OAAO,MAAM;SACV,KAAK,CAAC,WAAW,CAAC;SAClB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;SACpB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;SAC3B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;AACjD,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* This tool does NOT modify, tag, or delete any resources.
|
|
14
14
|
*/
|
|
15
15
|
export { type Analyzer, type AnalyzerContext, type AzureClients, createDefaultAnalyzers, } from "./azure/analyzers/index.js";
|
|
16
|
-
export type { AzureConfig, AzureSource } from "./azure/types.js";
|
|
16
|
+
export type { AzureConfig, AzureSource, PricingConfig } from "./azure/types.js";
|
|
17
|
+
export { type MetricsCache, type MonitorClientLike } from "./azure/utils.js";
|
|
17
18
|
import * as azureModule from "./azure/index.js";
|
|
18
19
|
export declare const azure: typeof azureModule;
|
|
19
|
-
export { type MetricsCache, type MonitorClientLike } from "./azure/utils.js";
|
|
20
20
|
export { type Finding, type FindingCategory, findingsFromAnalysisResult, type FindingSource, type Money, type ResourceReport, } from "./finding.js";
|
|
21
21
|
export * from "./types.js";
|
|
22
22
|
import type { AzureConfig } from "./azure/types.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EACL,KAAK,QAAQ,EACb,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAGpC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAG7E,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAChD,eAAO,MAAM,KAAK,oBAAc,CAAC;AAGjC,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,eAAe,EACpB,0BAA0B,EAC1B,KAAK,aAAa,EAClB,KAAK,KAAK,EACV,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AAEtB,cAAc,YAAY,CAAC;AAE3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAE1E"}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAIL,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAIL,sBAAsB,GACvB,MAAM,4BAA4B,CAAC;AAMpC,sBAAsB;AACtB,OAAO,KAAK,WAAW,MAAM,kBAAkB,CAAC;AAChD,MAAM,CAAC,MAAM,KAAK,GAAG,WAAW,CAAC;AAEjC,2DAA2D;AAC3D,OAAO,EAGL,0BAA0B,GAI3B,MAAM,cAAc,CAAC;AAEtB,cAAc,YAAY,CAAC;AAI3B,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAmB;IAClD,OAAO,eAAe,CAAC,UAAU,CAAC,CAAC;AACrC,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Common types shared across all cloud providers
|
|
3
3
|
*/
|
|
4
|
+
import type { Money } from "./finding.js";
|
|
4
5
|
export type AnalysisResult = {
|
|
5
6
|
costRisk: CostRisk;
|
|
7
|
+
/**
|
|
8
|
+
* Estimated monthly resource cost at risk. Custom pricing-enriched
|
|
9
|
+
* analyzers populate it when a Retail Prices lookup succeeds; the report
|
|
10
|
+
* renders it once per resource instead of treating it as a per-finding
|
|
11
|
+
* saving.
|
|
12
|
+
*/
|
|
13
|
+
estimatedMonthlySavings?: Money;
|
|
6
14
|
reason: string;
|
|
7
15
|
suspectedUnused: boolean;
|
|
8
16
|
};
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAI1C,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,EAAE,QAAQ,CAAC;IACnB;;;;;OAKG;IACH,uBAAuB,CAAC,EAAE,KAAK,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;CAC1B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjD;;;GAGG;AACH,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;CAA6B,CAAC;AAE7D;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,cAAc,GAC7B,cAAc,CAUhB"}
|
package/dist/types.js
CHANGED
|
@@ -13,6 +13,8 @@ export const DEFAULT_THRESHOLDS = ThresholdsSchema.parse({});
|
|
|
13
13
|
export function mergeResults(baseResult, specificResult) {
|
|
14
14
|
return {
|
|
15
15
|
costRisk: specificResult.costRisk,
|
|
16
|
+
estimatedMonthlySavings: specificResult.estimatedMonthlySavings ??
|
|
17
|
+
baseResult.estimatedMonthlySavings,
|
|
16
18
|
reason: baseResult.reason + specificResult.reason,
|
|
17
19
|
suspectedUnused: baseResult.suspectedUnused || specificResult.suspectedUnused,
|
|
18
20
|
};
|
package/dist/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA+B/C;;;GAGG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;AAE7D;;GAEG;AACH,MAAM,UAAU,YAAY,CAC1B,UAA0B,EAC1B,cAA8B;IAE9B,OAAO;QACL,QAAQ,EAAE,cAAc,CAAC,QAAQ;QACjC,uBAAuB,EACrB,cAAc,CAAC,uBAAuB;YACtC,UAAU,CAAC,uBAAuB;QACpC,MAAM,EAAE,UAAU,CAAC,MAAM,GAAG,cAAc,CAAC,MAAM;QACjD,eAAe,EACb,UAAU,CAAC,eAAe,IAAI,cAAc,CAAC,eAAe;KAC/D,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pagopa/dx-savemoney",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Azure resource analyzer for finding unused or cost-inefficient resources.",
|
|
6
6
|
"repository": {
|
|
@@ -35,22 +35,22 @@
|
|
|
35
35
|
"@azure/arm-network": "^34.2.0",
|
|
36
36
|
"@azure/arm-resources": "^7.0.0",
|
|
37
37
|
"@azure/identity": "^4.13.1",
|
|
38
|
-
"@logtape/logtape": "^1.3.
|
|
38
|
+
"@logtape/logtape": "^1.3.11",
|
|
39
39
|
"cli-table3": "^0.6.5",
|
|
40
|
-
"js-yaml": "^4.
|
|
40
|
+
"js-yaml": "^4.3.0",
|
|
41
41
|
"p-limit": "^7.3.0",
|
|
42
|
-
"zod": "^4.4.
|
|
42
|
+
"zod": "^4.4.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@tsconfig/node24": "24.0.4",
|
|
46
46
|
"@types/js-yaml": "^4.0.9",
|
|
47
|
-
"@types/node": "^22.
|
|
48
|
-
"@vitest/coverage-v8": "^4.1.
|
|
49
|
-
"eslint": "^10.
|
|
47
|
+
"@types/node": "^22.20.0",
|
|
48
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
49
|
+
"eslint": "^10.6.0",
|
|
50
50
|
"prettier": "3.8.3",
|
|
51
51
|
"typescript": "~5.9.3",
|
|
52
|
-
"vitest": "^4.1.
|
|
53
|
-
"@pagopa/eslint-config": "^6.
|
|
52
|
+
"vitest": "^4.1.9",
|
|
53
|
+
"@pagopa/eslint-config": "^6.2.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "rm -rf dist && tsc",
|
|
@@ -145,5 +145,29 @@ describe("findingsFromAnalysisResult", () => {
|
|
|
145
145
|
});
|
|
146
146
|
expect(result.every((f) => f.category === "cost")).toBe(true);
|
|
147
147
|
});
|
|
148
|
+
|
|
149
|
+
it("never attaches estimatedMonthlySavings to custom findings", () => {
|
|
150
|
+
// Custom analyzers carry the resource's monthly cost on
|
|
151
|
+
// `AnalysisResult.estimatedMonthlySavings` and the report layer
|
|
152
|
+
// renders it at the resource level. Propagating it down to a
|
|
153
|
+
// single finding sentence (e.g. "No tags found") would falsely
|
|
154
|
+
// suggest that fixing that sentence yields the full saving.
|
|
155
|
+
const result = findingsFromAnalysisResult({
|
|
156
|
+
...BASE_ARGS,
|
|
157
|
+
reason: "Low CPU. Low network. Idle.",
|
|
158
|
+
});
|
|
159
|
+
expect(result).toHaveLength(3);
|
|
160
|
+
for (const f of result) {
|
|
161
|
+
expect(f.estimatedMonthlySavings).toBeUndefined();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("omits estimatedMonthlySavings entirely", () => {
|
|
166
|
+
const result = findingsFromAnalysisResult({
|
|
167
|
+
...BASE_ARGS,
|
|
168
|
+
reason: "Low CPU.",
|
|
169
|
+
});
|
|
170
|
+
expect(result[0].estimatedMonthlySavings).toBeUndefined();
|
|
171
|
+
});
|
|
148
172
|
});
|
|
149
173
|
});
|
|
@@ -8,8 +8,12 @@
|
|
|
8
8
|
import { describe, expect, it } from "vitest";
|
|
9
9
|
|
|
10
10
|
import type { Finding } from "../../finding.js";
|
|
11
|
+
import type { AzureDetailedResourceReport } from "../types.js";
|
|
11
12
|
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
mergeFinding,
|
|
15
|
+
shouldIncludeAdvisorFindingForTags,
|
|
16
|
+
} from "../analyzer.js";
|
|
13
17
|
|
|
14
18
|
function mkFinding(resourceId: string, source: Finding["source"]): Finding {
|
|
15
19
|
return {
|
|
@@ -72,3 +76,34 @@ describe("shouldIncludeAdvisorFindingForTags", () => {
|
|
|
72
76
|
).toBe(false);
|
|
73
77
|
});
|
|
74
78
|
});
|
|
79
|
+
|
|
80
|
+
describe("mergeFinding", () => {
|
|
81
|
+
it("preserves custom cost at risk when merging an Advisor finding", () => {
|
|
82
|
+
const resourceId =
|
|
83
|
+
"/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1";
|
|
84
|
+
const existing: AzureDetailedResourceReport = {
|
|
85
|
+
analysis: {
|
|
86
|
+
costRisk: "high",
|
|
87
|
+
estimatedMonthlySavings: { amount: 29.94, currency: "EUR" },
|
|
88
|
+
reason: "VM is deallocated.",
|
|
89
|
+
suspectedUnused: true,
|
|
90
|
+
},
|
|
91
|
+
findings: [mkFinding(resourceId, "custom")],
|
|
92
|
+
resource: {
|
|
93
|
+
id: resourceId,
|
|
94
|
+
name: "vm1",
|
|
95
|
+
type: "Microsoft.Compute/virtualMachines",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const reports = [existing];
|
|
99
|
+
const reportsById = new Map([[resourceId.toLowerCase(), existing]]);
|
|
100
|
+
|
|
101
|
+
mergeFinding(mkFinding(resourceId, "advisor"), reports, reportsById);
|
|
102
|
+
|
|
103
|
+
expect(existing.analysis.estimatedMonthlySavings).toEqual({
|
|
104
|
+
amount: 29.94,
|
|
105
|
+
currency: "EUR",
|
|
106
|
+
});
|
|
107
|
+
expect(existing.findings).toHaveLength(2);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -11,6 +11,7 @@ import { fileURLToPath } from "url";
|
|
|
11
11
|
import { describe, expect, it } from "vitest";
|
|
12
12
|
|
|
13
13
|
import { loadConfig } from "../../index.js";
|
|
14
|
+
import { ConfigSchema } from "../../schema.js";
|
|
14
15
|
import { DEFAULT_THRESHOLDS } from "../../types.js";
|
|
15
16
|
|
|
16
17
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -94,4 +95,15 @@ describe("loadConfig", () => {
|
|
|
94
95
|
expect(result.thresholds).toHaveProperty("publicIp");
|
|
95
96
|
expect(result.thresholds).toHaveProperty("staticSite");
|
|
96
97
|
});
|
|
98
|
+
|
|
99
|
+
it("does not expose pricing settings in YAML config", () => {
|
|
100
|
+
const result = ConfigSchema.safeParse({
|
|
101
|
+
azure: {
|
|
102
|
+
pricing: { enabled: false },
|
|
103
|
+
subscriptionIds: ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"],
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(false);
|
|
108
|
+
});
|
|
97
109
|
});
|
|
@@ -133,6 +133,84 @@ describe("generateReport — lint format", () => {
|
|
|
133
133
|
expect(calls).toHaveLength(1);
|
|
134
134
|
expect(calls[0]).toContain("0 issues found");
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
describe("monetary annotations", () => {
|
|
138
|
+
const VM_WITH_COST: AzureDetailedResourceReport = {
|
|
139
|
+
analysis: {
|
|
140
|
+
costRisk: "high",
|
|
141
|
+
estimatedMonthlySavings: { amount: 29.94, currency: "EUR" },
|
|
142
|
+
reason: "No tags found. VM is deallocated.",
|
|
143
|
+
suspectedUnused: true,
|
|
144
|
+
},
|
|
145
|
+
resource: {
|
|
146
|
+
id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm-x",
|
|
147
|
+
name: "vm-x",
|
|
148
|
+
type: "Microsoft.Compute/virtualMachines",
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const ADVISOR_WITH_SAVINGS: AzureDetailedResourceReport = {
|
|
152
|
+
analysis: {
|
|
153
|
+
costRisk: "medium",
|
|
154
|
+
reason: "Advisor recommends a reservation.",
|
|
155
|
+
suspectedUnused: true,
|
|
156
|
+
},
|
|
157
|
+
findings: [
|
|
158
|
+
{
|
|
159
|
+
category: "cost",
|
|
160
|
+
code: "advisor.reservation",
|
|
161
|
+
estimatedMonthlySavings: { amount: 12.34, currency: "EUR" },
|
|
162
|
+
reason: "Advisor recommends a reservation.",
|
|
163
|
+
resourceId:
|
|
164
|
+
"/subscriptions/sub1/providers/Microsoft.Advisor/recommendations/rec-1",
|
|
165
|
+
severity: "medium",
|
|
166
|
+
source: "advisor",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
resource: {
|
|
170
|
+
id: "/subscriptions/sub1/providers/Microsoft.Advisor/recommendations/rec-1",
|
|
171
|
+
name: "rec-1",
|
|
172
|
+
type: "Microsoft.Advisor/recommendations",
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
it("renders the resource monthly cost ONCE as a header annotation, not on each finding", async () => {
|
|
177
|
+
await generateReport([VM_WITH_COST], "lint");
|
|
178
|
+
const output = allLogs(logSpy);
|
|
179
|
+
// header line contains the cost-at-risk label
|
|
180
|
+
expect(output).toMatch(/cost at risk:.*29[.,]94/);
|
|
181
|
+
// exactly one occurrence — must NOT appear next to each finding
|
|
182
|
+
const matches = output.match(/29[.,]94/g) ?? [];
|
|
183
|
+
// 1× header + 1× summary trailer = 2 occurrences; the per-finding line must not carry it
|
|
184
|
+
expect(matches.length).toBe(2);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("does not attach the resource cost to the 'No tags found' finding line", async () => {
|
|
188
|
+
await generateReport([VM_WITH_COST], "lint");
|
|
189
|
+
const findingLines = logSpy.mock.calls
|
|
190
|
+
.map((c: unknown[]) => String(c[0]))
|
|
191
|
+
.filter((l: string) => l.includes("No tags found"));
|
|
192
|
+
expect(findingLines).toHaveLength(1);
|
|
193
|
+
expect(findingLines[0]).not.toMatch(/29[.,]94/);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("uses the 'Estimated monthly cost at risk' label in the summary trailer", async () => {
|
|
197
|
+
await generateReport([VM_WITH_COST], "lint");
|
|
198
|
+
const output = allLogs(logSpy);
|
|
199
|
+
expect(output).toContain("Estimated monthly cost at risk");
|
|
200
|
+
// The old, misleading "Estimated monthly savings (custom)" label is gone
|
|
201
|
+
expect(output).not.toMatch(/Estimated monthly savings:\s*€/);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("renders Advisor savings separately from custom cost at risk", async () => {
|
|
205
|
+
await generateReport([VM_WITH_COST, ADVISOR_WITH_SAVINGS], "lint");
|
|
206
|
+
const output = allLogs(logSpy);
|
|
207
|
+
|
|
208
|
+
expect(output).toMatch(/cost at risk:.*29[.,]94/);
|
|
209
|
+
expect(output).toMatch(/saving:.*12[.,]34/);
|
|
210
|
+
expect(output).toContain("Estimated monthly cost at risk (custom)");
|
|
211
|
+
expect(output).toContain("Estimated monthly savings (advisor)");
|
|
212
|
+
});
|
|
213
|
+
});
|
|
136
214
|
});
|
|
137
215
|
|
|
138
216
|
describe("generateReport — table format", () => {
|