@pagopa/dx-savemoney 0.1.5 → 0.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.
Files changed (76) hide show
  1. package/README.md +104 -27
  2. package/dist/azure/analyzer.d.ts +4 -4
  3. package/dist/azure/analyzer.d.ts.map +1 -1
  4. package/dist/azure/analyzer.js +16 -10
  5. package/dist/azure/analyzer.js.map +1 -1
  6. package/dist/azure/config.d.ts +15 -3
  7. package/dist/azure/config.d.ts.map +1 -1
  8. package/dist/azure/config.js +32 -16
  9. package/dist/azure/config.js.map +1 -1
  10. package/dist/azure/report.d.ts +2 -2
  11. package/dist/azure/report.d.ts.map +1 -1
  12. package/dist/azure/report.js +68 -3
  13. package/dist/azure/report.js.map +1 -1
  14. package/dist/azure/resources/app-service.d.ts +2 -2
  15. package/dist/azure/resources/app-service.d.ts.map +1 -1
  16. package/dist/azure/resources/app-service.js +8 -5
  17. package/dist/azure/resources/app-service.js.map +1 -1
  18. package/dist/azure/resources/container-app.d.ts +2 -2
  19. package/dist/azure/resources/container-app.d.ts.map +1 -1
  20. package/dist/azure/resources/container-app.js +10 -8
  21. package/dist/azure/resources/container-app.js.map +1 -1
  22. package/dist/azure/resources/public-ip.d.ts +2 -2
  23. package/dist/azure/resources/public-ip.d.ts.map +1 -1
  24. package/dist/azure/resources/public-ip.js +3 -3
  25. package/dist/azure/resources/public-ip.js.map +1 -1
  26. package/dist/azure/resources/static-web-app.d.ts +2 -2
  27. package/dist/azure/resources/static-web-app.d.ts.map +1 -1
  28. package/dist/azure/resources/static-web-app.js +4 -5
  29. package/dist/azure/resources/static-web-app.js.map +1 -1
  30. package/dist/azure/resources/storage.d.ts +2 -2
  31. package/dist/azure/resources/storage.d.ts.map +1 -1
  32. package/dist/azure/resources/storage.js +4 -3
  33. package/dist/azure/resources/storage.js.map +1 -1
  34. package/dist/azure/resources/vm.d.ts +2 -2
  35. package/dist/azure/resources/vm.d.ts.map +1 -1
  36. package/dist/azure/resources/vm.js +4 -5
  37. package/dist/azure/resources/vm.js.map +1 -1
  38. package/dist/azure/types.d.ts +10 -2
  39. package/dist/azure/types.d.ts.map +1 -1
  40. package/dist/azure/utils.d.ts +9 -0
  41. package/dist/azure/utils.d.ts.map +1 -1
  42. package/dist/azure/utils.js +14 -0
  43. package/dist/azure/utils.js.map +1 -1
  44. package/dist/index.d.ts +16 -11
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +18 -54
  47. package/dist/index.js.map +1 -1
  48. package/dist/schema.d.ts +210 -0
  49. package/dist/schema.d.ts.map +1 -0
  50. package/dist/schema.js +97 -0
  51. package/dist/schema.js.map +1 -0
  52. package/dist/types.d.ts +35 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +6 -0
  55. package/dist/types.js.map +1 -1
  56. package/package.json +12 -6
  57. package/src/azure/__tests__/config.test.ts +97 -0
  58. package/src/azure/__tests__/fixtures/full-override.yaml +25 -0
  59. package/src/azure/__tests__/fixtures/partial-override.yaml +10 -0
  60. package/src/azure/__tests__/report.test.ts +138 -0
  61. package/src/azure/__tests__/utils.test.ts +124 -0
  62. package/src/azure/analyzer.ts +24 -3
  63. package/src/azure/config.ts +33 -21
  64. package/src/azure/report.ts +81 -4
  65. package/src/azure/resources/__tests__/storage.test.ts +185 -0
  66. package/src/azure/resources/app-service.ts +13 -5
  67. package/src/azure/resources/container-app.ts +13 -4
  68. package/src/azure/resources/public-ip.ts +4 -3
  69. package/src/azure/resources/static-web-app.ts +5 -5
  70. package/src/azure/resources/storage.ts +7 -3
  71. package/src/azure/resources/vm.ts +5 -5
  72. package/src/azure/types.ts +15 -2
  73. package/src/azure/utils.ts +21 -0
  74. package/src/index.ts +19 -69
  75. package/src/schema.ts +134 -0
  76. package/src/types.ts +14 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Tests for analyzeStorageAccount() — verifies that custom thresholds
3
+ * actually change the analysis outcome.
4
+ *
5
+ * Key scenario:
6
+ * - Metric: 30 avg transactions/day
7
+ * - Default threshold: 10 → 30 >= 10 → NOT flagged
8
+ * - Custom threshold: 50 → 30 < 50 → IS flagged ✓ proves the feature works
9
+ */
10
+
11
+ import type { MonitorClient } from "@azure/arm-monitor";
12
+ import type { GenericResource } from "@azure/arm-resources";
13
+
14
+ import { beforeEach, describe, expect, it, vi } from "vitest";
15
+
16
+ import type { Thresholds } from "../../../types.js";
17
+
18
+ import { DEFAULT_THRESHOLDS } from "../../../types.js";
19
+
20
+ // Mock the utils module so we control getMetric without real Azure calls
21
+ vi.mock("../../utils.js", () => ({
22
+ getMetric: vi.fn(),
23
+ verboseLog: vi.fn(),
24
+ verboseLogAnalysisResult: vi.fn(),
25
+ verboseLogResourceStart: vi.fn(),
26
+ }));
27
+
28
+ // Import after vi.mock so the module resolves the mock
29
+ import { getMetric } from "../../utils.js";
30
+ import { analyzeStorageAccount } from "../storage.js";
31
+
32
+ // ── helpers ────────────────────────────────────────────────────────────────
33
+
34
+ // Minimal stub: analyzeStorageAccount only needs monitorClient to be passed
35
+ // through to getMetric, which is fully mocked. We provide only the shape that
36
+ // TypeScript requires without unsafe type assertions.
37
+ const FAKE_CLIENT = {
38
+ metrics: { list: vi.fn() },
39
+ } as unknown as MonitorClient;
40
+
41
+ const FAKE_RESOURCE: GenericResource = {
42
+ id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Storage/storageAccounts/st1",
43
+ name: "st1",
44
+ type: "Microsoft.Storage/storageAccounts",
45
+ };
46
+
47
+ const mockGetMetric = vi.mocked(getMetric);
48
+
49
+ // ── tests ──────────────────────────────────────────────────────────────────
50
+
51
+ describe("analyzeStorageAccount — threshold sensitivity", () => {
52
+ beforeEach(() => {
53
+ mockGetMetric.mockReset();
54
+ });
55
+
56
+ describe("with DEFAULT threshold (transactionsPerDay = 10)", () => {
57
+ it("does NOT flag a resource with 30 transactions/day (30 ≥ 10)", async () => {
58
+ mockGetMetric.mockResolvedValue(30);
59
+
60
+ const result = await analyzeStorageAccount(
61
+ FAKE_RESOURCE,
62
+ FAKE_CLIENT,
63
+ 30,
64
+ DEFAULT_THRESHOLDS,
65
+ );
66
+
67
+ expect(result.suspectedUnused).toBe(false);
68
+ });
69
+
70
+ it("flags a resource with 5 transactions/day (5 < 10)", async () => {
71
+ mockGetMetric.mockResolvedValue(5);
72
+
73
+ const result = await analyzeStorageAccount(
74
+ FAKE_RESOURCE,
75
+ FAKE_CLIENT,
76
+ 30,
77
+ DEFAULT_THRESHOLDS,
78
+ );
79
+
80
+ expect(result.suspectedUnused).toBe(true);
81
+ });
82
+
83
+ it("does not flag when metric is exactly at the threshold (10)", async () => {
84
+ mockGetMetric.mockResolvedValue(10);
85
+
86
+ const result = await analyzeStorageAccount(
87
+ FAKE_RESOURCE,
88
+ FAKE_CLIENT,
89
+ 30,
90
+ DEFAULT_THRESHOLDS,
91
+ );
92
+
93
+ // 10 < 10 is false → not flagged
94
+ expect(result.suspectedUnused).toBe(false);
95
+ });
96
+ });
97
+
98
+ describe("with CUSTOM threshold (transactionsPerDay = 50)", () => {
99
+ const customThresholds: Thresholds = {
100
+ ...DEFAULT_THRESHOLDS,
101
+ storage: { transactionsPerDay: 50 },
102
+ };
103
+
104
+ it("DOES flag a resource with 30 transactions/day (30 < 50) — proves override works", async () => {
105
+ mockGetMetric.mockResolvedValue(30);
106
+
107
+ const result = await analyzeStorageAccount(
108
+ FAKE_RESOURCE,
109
+ FAKE_CLIENT,
110
+ 30,
111
+ customThresholds,
112
+ );
113
+
114
+ expect(result.suspectedUnused).toBe(true);
115
+ });
116
+
117
+ it("does NOT flag a resource with 60 transactions/day (60 ≥ 50)", async () => {
118
+ mockGetMetric.mockResolvedValue(60);
119
+
120
+ const result = await analyzeStorageAccount(
121
+ FAKE_RESOURCE,
122
+ FAKE_CLIENT,
123
+ 30,
124
+ customThresholds,
125
+ );
126
+
127
+ expect(result.suspectedUnused).toBe(false);
128
+ });
129
+
130
+ it("flags at threshold boundary (49 < 50)", async () => {
131
+ mockGetMetric.mockResolvedValue(49);
132
+
133
+ const result = await analyzeStorageAccount(
134
+ FAKE_RESOURCE,
135
+ FAKE_CLIENT,
136
+ 30,
137
+ customThresholds,
138
+ );
139
+
140
+ expect(result.suspectedUnused).toBe(true);
141
+ });
142
+ });
143
+
144
+ describe("edge cases", () => {
145
+ it("returns suspectedUnused: false when metric is null (data unavailable)", async () => {
146
+ mockGetMetric.mockResolvedValue(null);
147
+
148
+ const result = await analyzeStorageAccount(
149
+ FAKE_RESOURCE,
150
+ FAKE_CLIENT,
151
+ 30,
152
+ DEFAULT_THRESHOLDS,
153
+ );
154
+
155
+ expect(result.suspectedUnused).toBe(false);
156
+ });
157
+
158
+ it("returns suspectedUnused: false when resource id is missing", async () => {
159
+ const resourceWithoutId = { ...FAKE_RESOURCE, id: undefined };
160
+
161
+ const result = await analyzeStorageAccount(
162
+ resourceWithoutId,
163
+ FAKE_CLIENT,
164
+ 30,
165
+ DEFAULT_THRESHOLDS,
166
+ );
167
+
168
+ expect(result.suspectedUnused).toBe(false);
169
+ expect(mockGetMetric).not.toHaveBeenCalled();
170
+ });
171
+
172
+ it("applies default thresholds when the parameter is omitted", async () => {
173
+ // 5 < DEFAULT 10 → flagged even without explicit thresholds argument
174
+ mockGetMetric.mockResolvedValue(5);
175
+
176
+ const result = await analyzeStorageAccount(
177
+ FAKE_RESOURCE,
178
+ FAKE_CLIENT,
179
+ 30,
180
+ );
181
+
182
+ expect(result.suspectedUnused).toBe(true);
183
+ });
184
+ });
185
+ });
@@ -8,8 +8,9 @@ import type { MonitorClient } from "@azure/arm-monitor";
8
8
  import * as armResources from "@azure/arm-resources";
9
9
  import { getLogger } from "@logtape/logtape";
10
10
 
11
- import type { AnalysisResult } from "../../types.js";
11
+ import type { AnalysisResult, Thresholds } from "../../types.js";
12
12
 
13
+ import { DEFAULT_THRESHOLDS } from "../../types.js";
13
14
  import {
14
15
  getMetric,
15
16
  verboseLog,
@@ -31,6 +32,7 @@ export async function analyzeAppServicePlan(
31
32
  webSiteClient: WebSiteManagementClient,
32
33
  monitorClient: MonitorClient,
33
34
  timespanDays: number,
35
+ thresholds: Thresholds = DEFAULT_THRESHOLDS,
34
36
  verbose = false,
35
37
  ): Promise<AnalysisResult> {
36
38
  verboseLogResourceStart(
@@ -87,19 +89,25 @@ export async function analyzeAppServicePlan(
87
89
  timespanDays,
88
90
  );
89
91
 
90
- if (cpuPercentage !== null && cpuPercentage < 5) {
92
+ if (
93
+ cpuPercentage !== null &&
94
+ cpuPercentage < thresholds.appService.cpuPercent
95
+ ) {
91
96
  reason += `Very low CPU usage (${cpuPercentage.toFixed(2)}%). `;
92
97
  }
93
98
 
94
- if (memoryPercentage !== null && memoryPercentage < 10) {
99
+ if (
100
+ memoryPercentage !== null &&
101
+ memoryPercentage < thresholds.appService.memoryPercent
102
+ ) {
95
103
  reason += `Very low memory usage (${memoryPercentage.toFixed(2)}%). `;
96
104
  }
97
105
 
98
106
  // Check if it's an oversized plan (Premium tier with low usage)
99
107
  if (
100
108
  planDetails.sku?.tier?.includes("Premium") &&
101
- cpuPercentage &&
102
- cpuPercentage < 10
109
+ cpuPercentage !== null &&
110
+ cpuPercentage < thresholds.appService.premiumCpuPercent
103
111
  ) {
104
112
  reason += "Premium tier with low resource utilization. ";
105
113
  }
@@ -8,8 +8,9 @@ import type { MonitorClient } from "@azure/arm-monitor";
8
8
  import * as armResources from "@azure/arm-resources";
9
9
  import { getLogger } from "@logtape/logtape";
10
10
 
11
- import type { AnalysisResult } from "../../types.js";
11
+ import type { AnalysisResult, Thresholds } from "../../types.js";
12
12
 
13
+ import { DEFAULT_THRESHOLDS } from "../../types.js";
13
14
  import {
14
15
  getMetric,
15
16
  verboseLog,
@@ -32,6 +33,7 @@ export async function analyzeContainerApp(
32
33
  containerAppsClient: ContainerAppsAPIClient,
33
34
  monitorClient: MonitorClient,
34
35
  timespanDays: number,
36
+ thresholds: Thresholds = DEFAULT_THRESHOLDS,
35
37
  verbose = false,
36
38
  ): Promise<AnalysisResult> {
37
39
  verboseLogResourceStart(
@@ -79,6 +81,7 @@ export async function analyzeContainerApp(
79
81
  resource,
80
82
  monitorClient,
81
83
  timespanDays,
84
+ thresholds,
82
85
  reason,
83
86
  verbose,
84
87
  );
@@ -86,6 +89,7 @@ export async function analyzeContainerApp(
86
89
  resource,
87
90
  monitorClient,
88
91
  timespanDays,
92
+ thresholds,
89
93
  reason,
90
94
  verbose,
91
95
  );
@@ -110,6 +114,7 @@ async function checkNetworkMetrics(
110
114
  resource: armResources.GenericResource,
111
115
  monitorClient: MonitorClient,
112
116
  timespanDays: number,
117
+ thresholds: Thresholds,
113
118
  reason: string,
114
119
  verbose: boolean,
115
120
  ): Promise<string> {
@@ -149,7 +154,7 @@ async function checkNetworkMetrics(
149
154
  if (
150
155
  networkIn !== null &&
151
156
  networkOut !== null &&
152
- networkIn + networkOut < 34000
157
+ networkIn + networkOut < thresholds.containerApp.networkBytes
153
158
  ) {
154
159
  newReason += `Very low network traffic (${((networkIn + networkOut) / 1048576).toFixed(2)} MB/day avg). `;
155
160
  }
@@ -164,6 +169,7 @@ async function checkResourceMetrics(
164
169
  resource: armResources.GenericResource,
165
170
  monitorClient: MonitorClient,
166
171
  timespanDays: number,
172
+ thresholds: Thresholds,
167
173
  reason: string,
168
174
  verbose: boolean,
169
175
  ): Promise<string> {
@@ -200,11 +206,14 @@ async function checkResourceMetrics(
200
206
  `Memory Usage: ${memoryUsage !== null ? `${(memoryUsage / 1048576).toFixed(2)} MB` : "N/A"}`,
201
207
  );
202
208
 
203
- if (cpuUsage !== null && cpuUsage < 1000000) {
209
+ if (cpuUsage !== null && cpuUsage < thresholds.containerApp.cpuNanoCores) {
204
210
  newReason += `Very low CPU usage (${(cpuUsage / 1000000000).toFixed(4)} cores). `;
205
211
  }
206
212
 
207
- if (memoryUsage !== null && memoryUsage < 10485760) {
213
+ if (
214
+ memoryUsage !== null &&
215
+ memoryUsage < thresholds.containerApp.memoryBytes
216
+ ) {
208
217
  newReason += `Very low memory usage (${(memoryUsage / 1048576).toFixed(2)} MB). `;
209
218
  }
210
219
 
@@ -8,8 +8,9 @@ import type { NetworkManagementClient } from "@azure/arm-network";
8
8
  import * as armResources from "@azure/arm-resources";
9
9
  import { getLogger } from "@logtape/logtape";
10
10
 
11
- import type { AnalysisResult } from "../../types.js";
11
+ import type { AnalysisResult, Thresholds } from "../../types.js";
12
12
 
13
+ import { DEFAULT_THRESHOLDS } from "../../types.js";
13
14
  import {
14
15
  getMetric,
15
16
  verboseLog,
@@ -31,6 +32,7 @@ export async function analyzePublicIp(
31
32
  networkClient: NetworkManagementClient,
32
33
  monitorClient: MonitorClient,
33
34
  timespanDays: number,
35
+ thresholds: Thresholds = DEFAULT_THRESHOLDS,
34
36
  verbose = false,
35
37
  ): Promise<AnalysisResult> {
36
38
  verboseLogResourceStart(
@@ -87,8 +89,7 @@ export async function analyzePublicIp(
87
89
  timespanDays,
88
90
  );
89
91
 
90
- if (bytesInDDoS !== null && bytesInDDoS < 340000) {
91
- // Less than ~340KB average per day
92
+ if (bytesInDDoS !== null && bytesInDDoS < thresholds.publicIp.bytesInDDoS) {
92
93
  reason += `Very low network traffic (${(bytesInDDoS / 1024 / 1024).toFixed(2)} MB/day avg). `;
93
94
  }
94
95
  } catch (error) {
@@ -7,8 +7,9 @@ import type { MonitorClient } from "@azure/arm-monitor";
7
7
  import * as armResources from "@azure/arm-resources";
8
8
  import { getLogger } from "@logtape/logtape";
9
9
 
10
- import type { AnalysisResult } from "../../types.js";
10
+ import type { AnalysisResult, Thresholds } from "../../types.js";
11
11
 
12
+ import { DEFAULT_THRESHOLDS } from "../../types.js";
12
13
  import {
13
14
  getMetric,
14
15
  verboseLog,
@@ -29,6 +30,7 @@ export async function analyzeStaticSite(
29
30
  resource: armResources.GenericResource,
30
31
  monitorClient: MonitorClient,
31
32
  timespanDays: number,
33
+ thresholds: Thresholds = DEFAULT_THRESHOLDS,
32
34
  verbose = false,
33
35
  ): Promise<AnalysisResult> {
34
36
  verboseLogResourceStart(
@@ -84,13 +86,11 @@ export async function analyzeStaticSite(
84
86
  if (siteHits === null && bytesSent === null) {
85
87
  reason += `No traffic data available in ${timespanDays} days. `;
86
88
  } else {
87
- if (siteHits !== null && siteHits < 100) {
88
- // Less than 100 requests total in the timespan (< ~3.3 requests/day)
89
+ if (siteHits !== null && siteHits < thresholds.staticSite.siteHits) {
89
90
  reason += `Very low site traffic (${siteHits.toFixed(0)} requests in ${timespanDays} days). `;
90
91
  }
91
92
 
92
- if (bytesSent !== null && bytesSent < 1048576) {
93
- // Less than 1MB total in the timespan (< ~34KB/day)
93
+ if (bytesSent !== null && bytesSent < thresholds.staticSite.bytesSent) {
94
94
  reason += `Very low data transfer (${(bytesSent / 1024 / 1024).toFixed(2)} MB in ${timespanDays} days). `;
95
95
  }
96
96
  }
@@ -6,8 +6,9 @@ import type { MonitorClient } from "@azure/arm-monitor";
6
6
 
7
7
  import * as armResources from "@azure/arm-resources";
8
8
 
9
- import type { AnalysisResult } from "../../types.js";
9
+ import type { AnalysisResult, Thresholds } from "../../types.js";
10
10
 
11
+ import { DEFAULT_THRESHOLDS } from "../../types.js";
11
12
  import {
12
13
  getMetric,
13
14
  verboseLog,
@@ -27,6 +28,7 @@ export async function analyzeStorageAccount(
27
28
  resource: armResources.GenericResource,
28
29
  monitorClient: MonitorClient,
29
30
  timespanDays: number,
31
+ thresholds: Thresholds = DEFAULT_THRESHOLDS,
30
32
  verbose = false,
31
33
  ): Promise<AnalysisResult> {
32
34
  verboseLogResourceStart(
@@ -51,8 +53,10 @@ export async function analyzeStorageAccount(
51
53
  "Average",
52
54
  timespanDays,
53
55
  );
54
- if (transactions !== null && transactions < 10) {
55
- // Less than 10 transactions per day on average
56
+ if (
57
+ transactions !== null &&
58
+ transactions < thresholds.storage.transactionsPerDay
59
+ ) {
56
60
  const result = {
57
61
  costRisk,
58
62
  reason: `Very low transaction count (${transactions.toFixed(2)} avg/day). `,
@@ -8,8 +8,9 @@ import type { MonitorClient } from "@azure/arm-monitor";
8
8
  import * as armResources from "@azure/arm-resources";
9
9
  import { getLogger } from "@logtape/logtape";
10
10
 
11
- import type { AnalysisResult } from "../../types.js";
11
+ import type { AnalysisResult, Thresholds } from "../../types.js";
12
12
 
13
+ import { DEFAULT_THRESHOLDS } from "../../types.js";
13
14
  import {
14
15
  getMetric,
15
16
  verboseLog,
@@ -32,6 +33,7 @@ export async function analyzeVM(
32
33
  monitorClient: MonitorClient,
33
34
  computeClient: ComputeManagementClient,
34
35
  timespanDays: number,
36
+ thresholds: Thresholds = DEFAULT_THRESHOLDS,
35
37
  verbose = false,
36
38
  ): Promise<AnalysisResult> {
37
39
  verboseLogResourceStart(
@@ -114,12 +116,10 @@ export async function analyzeVM(
114
116
  timespanDays,
115
117
  );
116
118
 
117
- if (cpuUsage !== null && cpuUsage < 1) {
118
- // Less than 1% average CPU
119
+ if (cpuUsage !== null && cpuUsage < thresholds.vm.cpuPercent) {
119
120
  reason += `Low CPU usage (avg ${cpuUsage.toFixed(2)}%). `;
120
121
  }
121
- if (networkIn !== null && networkIn < 1024 * 1024 * 3) {
122
- // Less than 3MB average per day
122
+ if (networkIn !== null && networkIn < thresholds.vm.networkInBytesPerDay) {
123
123
  reason += `Low network traffic (${(networkIn / 1024 / 1024).toFixed(2)} MB/day avg). `;
124
124
  }
125
125
 
@@ -4,14 +4,27 @@
4
4
 
5
5
  import type * as armResources from "@azure/arm-resources";
6
6
 
7
- import type { AnalysisResult, BaseConfig, CostRisk } from "../types.js";
7
+ import type {
8
+ AnalysisResult,
9
+ BaseConfig,
10
+ CostRisk,
11
+ Thresholds,
12
+ } from "../types.js";
8
13
 
9
14
  /**
10
15
  * Azure configuration extending base config
11
16
  */
12
17
  export type AzureConfig = BaseConfig & {
18
+ /**
19
+ * Only analyze resources that match ALL the given tag key-value pairs.
20
+ * If omitted, all resources are analyzed.
21
+ */
22
+ filterTags?: Map<string, string>;
13
23
  subscriptionIds: string[];
14
- tenantId: string;
24
+ /**
25
+ * Analysis thresholds. Defaults from DEFAULT_THRESHOLDS are used when not provided.
26
+ */
27
+ thresholds?: Thresholds;
15
28
  verbose?: boolean;
16
29
  };
17
30
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import type { MonitorClient } from "@azure/arm-monitor";
6
+ import type * as armResources from "@azure/arm-resources";
6
7
 
7
8
  import { getLogger } from "@logtape/logtape";
8
9
 
@@ -158,6 +159,26 @@ export async function getMetric(
158
159
  }
159
160
  }
160
161
 
162
+ /**
163
+ * Returns true if the resource matches ALL specified tag key-value pairs.
164
+ * If filterTags is empty or undefined, always returns true (no filtering).
165
+ *
166
+ * @param resource - The Azure resource to check
167
+ * @param filterTags - Map of required tag key→value pairs
168
+ */
169
+ export function matchesTags(
170
+ resource: armResources.GenericResource,
171
+ filterTags: Map<string, string> | undefined,
172
+ ): boolean {
173
+ if (!filterTags || filterTags.size === 0) {
174
+ return true;
175
+ }
176
+ const resourceTags = resource.tags ?? {};
177
+ return [...filterTags.entries()].every(
178
+ ([key, value]) => resourceTags[key] === value,
179
+ );
180
+ }
181
+
161
182
  /**
162
183
  * Logs a verbose message, optionally with an object.
163
184
  *
package/src/index.ts CHANGED
@@ -15,85 +15,35 @@
15
15
 
16
16
  // Export common types
17
17
  export type { AzureConfig } from "./azure/types.js";
18
- export * from "./types.js";
19
18
 
20
19
  // Export Azure module
21
20
  import * as azureModule from "./azure/index.js";
22
21
  export const azure = azureModule;
23
22
 
24
- import { getLogger } from "@logtape/logtape";
25
- // Utility imports for loadConfig and prompt functions
26
- import * as fs from "fs";
27
- import * as readline from "readline";
23
+ export * from "./types.js";
28
24
 
29
25
  import type { AzureConfig } from "./azure/types.js";
30
26
 
31
- /**
32
- * Loads configuration from file, environment variables, or interactive prompts.
33
- *
34
- * @param configPath - Optional path to JSON configuration file
35
- * @returns Configuration object with subscription IDs and settings
36
- */
37
- export async function loadConfig(configPath?: string): Promise<AzureConfig> {
38
- const logger = getLogger(["savemoney", "config"]);
39
-
40
- if (configPath && fs.existsSync(configPath)) {
41
- try {
42
- const configContent = fs.readFileSync(configPath, "utf-8");
43
- const config = JSON.parse(configContent);
44
-
45
- // Validate required fields
46
- if (!config.tenantId || !config.subscriptionIds) {
47
- throw new Error(
48
- "Config file must contain 'tenantId' and 'subscriptionIds'",
49
- );
50
- }
51
-
52
- return {
53
- ...config,
54
- preferredLocation: config.preferredLocation || "italynorth",
55
- timespanDays: config.timespanDays || 30,
56
- };
57
- } catch (error) {
58
- throw new Error(
59
- `Failed to load config file: ${error instanceof Error ? error.message : error}`,
60
- );
61
- }
62
- }
63
-
64
- logger.info(
65
- "Configuration file not found. Checking environment variables...",
66
- );
67
-
68
- const tenantId =
69
- process.env.ARM_TENANT_ID || (await prompt("Enter Tenant ID: "));
70
- const subscriptionIds = process.env.ARM_SUBSCRIPTION_ID
71
- ? process.env.ARM_SUBSCRIPTION_ID.split(",")
72
- : (await prompt("Enter Subscription IDs (comma-separated): ")).split(",");
73
-
74
- return {
75
- preferredLocation: "italynorth",
76
- subscriptionIds,
77
- tenantId,
78
- timespanDays: 30,
79
- };
80
- }
27
+ import { loadAzureConfig } from "./azure/config.js";
81
28
 
82
29
  /**
83
- * Prompts user for input via stdin.
30
+ * Loads configuration from a YAML file, environment variables, or interactive prompts.
31
+ *
32
+ * The YAML file should have an `azure` top-level key:
33
+ * ```yaml
34
+ * azure:
35
+ * subscriptionIds:
36
+ * - xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
37
+ * preferredLocation: italynorth
38
+ * timespanDays: 30
39
+ * thresholds:
40
+ * vm:
41
+ * cpuPercent: 5
42
+ * ```
84
43
  *
85
- * @param question - The question to display to the user
86
- * @returns User's input as a string
44
+ * @param configPath - Optional path to a YAML configuration file
45
+ * @returns Configuration object with subscription IDs, settings and thresholds
87
46
  */
88
- export async function prompt(question: string): Promise<string> {
89
- const rl = readline.createInterface({
90
- input: process.stdin,
91
- output: process.stdout,
92
- });
93
- return new Promise((resolve) =>
94
- rl.question(question, (answer) => {
95
- rl.close();
96
- resolve(answer);
97
- }),
98
- );
47
+ export async function loadConfig(configPath?: string): Promise<AzureConfig> {
48
+ return loadAzureConfig(configPath);
99
49
  }