@pagopa/dx-savemoney 0.2.5 → 0.2.6
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/dist/azure/analyzer.d.ts +31 -16
- package/dist/azure/analyzer.d.ts.map +1 -1
- package/dist/azure/analyzer.js +110 -81
- package/dist/azure/analyzer.js.map +1 -1
- package/dist/azure/analyzers/index.d.ts +6 -0
- package/dist/azure/analyzers/index.d.ts.map +1 -0
- package/dist/azure/analyzers/index.js +5 -0
- package/dist/azure/analyzers/index.js.map +1 -0
- package/dist/azure/analyzers/registry.d.ts +21 -0
- package/dist/azure/analyzers/registry.d.ts.map +1 -0
- package/dist/azure/analyzers/registry.js +69 -0
- package/dist/azure/analyzers/registry.js.map +1 -0
- package/dist/azure/analyzers/types.d.ts +62 -0
- package/dist/azure/analyzers/types.d.ts.map +1 -0
- package/dist/azure/analyzers/types.js +15 -0
- package/dist/azure/analyzers/types.js.map +1 -0
- package/dist/azure/config.d.ts.map +1 -1
- package/dist/azure/config.js +1 -0
- package/dist/azure/config.js.map +1 -1
- package/dist/azure/resources/app-service.d.ts +2 -1
- package/dist/azure/resources/app-service.d.ts.map +1 -1
- package/dist/azure/resources/app-service.js +3 -3
- package/dist/azure/resources/app-service.js.map +1 -1
- package/dist/azure/resources/container-app.d.ts +2 -1
- package/dist/azure/resources/container-app.d.ts.map +1 -1
- package/dist/azure/resources/container-app.js +9 -9
- package/dist/azure/resources/container-app.js.map +1 -1
- package/dist/azure/resources/public-ip.d.ts +2 -1
- package/dist/azure/resources/public-ip.d.ts.map +1 -1
- package/dist/azure/resources/public-ip.js +2 -2
- package/dist/azure/resources/public-ip.js.map +1 -1
- package/dist/azure/resources/static-web-app.d.ts +2 -1
- package/dist/azure/resources/static-web-app.d.ts.map +1 -1
- package/dist/azure/resources/static-web-app.js +3 -3
- package/dist/azure/resources/static-web-app.js.map +1 -1
- package/dist/azure/resources/storage.d.ts +2 -1
- package/dist/azure/resources/storage.d.ts.map +1 -1
- package/dist/azure/resources/storage.js +2 -2
- package/dist/azure/resources/storage.js.map +1 -1
- package/dist/azure/resources/vm.d.ts +2 -1
- package/dist/azure/resources/vm.d.ts.map +1 -1
- package/dist/azure/resources/vm.js +3 -3
- package/dist/azure/resources/vm.js.map +1 -1
- package/dist/azure/types.d.ts +6 -0
- package/dist/azure/types.d.ts.map +1 -1
- package/dist/azure/utils.d.ts +35 -3
- package/dist/azure/utils.d.ts.map +1 -1
- package/dist/azure/utils.js +70 -29
- package/dist/azure/utils.js.map +1 -1
- package/dist/finding.d.ts +114 -0
- package/dist/finding.d.ts.map +1 -0
- package/dist/finding.js +51 -0
- package/dist/finding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +5 -0
- package/dist/schema.js.map +1 -1
- package/package.json +2 -1
- package/src/__tests__/finding.test.ts +149 -0
- package/src/azure/__tests__/utils.test.ts +164 -2
- package/src/azure/analyzer.ts +140 -165
- package/src/azure/analyzers/index.ts +6 -0
- package/src/azure/analyzers/registry.ts +184 -0
- package/src/azure/analyzers/types.ts +66 -0
- package/src/azure/config.ts +1 -0
- package/src/azure/resources/app-service.ts +4 -0
- package/src/azure/resources/container-app.ts +10 -0
- package/src/azure/resources/public-ip.ts +3 -0
- package/src/azure/resources/static-web-app.ts +4 -0
- package/src/azure/resources/storage.ts +3 -0
- package/src/azure/resources/vm.ts +4 -0
- package/src/azure/types.ts +6 -0
- package/src/azure/utils.ts +110 -39
- package/src/finding.ts +152 -0
- package/src/index.ts +18 -0
- package/src/schema.ts +5 -0
package/src/azure/analyzer.ts
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Azure resource analyzer - Main orchestration logic
|
|
3
|
+
*
|
|
4
|
+
* Iterates every resource in every configured subscription, dispatches it
|
|
5
|
+
* to the registered analyzers (see `./analyzers/registry.ts`), and feeds
|
|
6
|
+
* the resulting findings into the report generator.
|
|
7
|
+
*
|
|
8
|
+
* Phase 0 refactor:
|
|
9
|
+
* - Resources are dispatched through a plugin-style `Analyzer` registry
|
|
10
|
+
* instead of a hard-coded `switch` statement.
|
|
11
|
+
* - Per-subscription resources are analyzed in parallel via a small
|
|
12
|
+
* in-process limiter (default concurrency 8, configurable via
|
|
13
|
+
* `AzureConfig.concurrency`).
|
|
14
|
+
* - Azure Monitor metric calls are memoized for the duration of a run via
|
|
15
|
+
* a per-run `MetricsCache` passed through `AnalyzerContext`, so concurrent
|
|
16
|
+
* calls to `analyzeAzureResources` stay fully isolated.
|
|
17
|
+
*
|
|
18
|
+
* The output schema (`AzureDetailedResourceReport`) is unchanged so the
|
|
19
|
+
* existing report formats keep working untouched.
|
|
3
20
|
*/
|
|
4
21
|
|
|
5
22
|
import { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
|
|
@@ -10,6 +27,7 @@ import { NetworkManagementClient } from "@azure/arm-network";
|
|
|
10
27
|
import * as armResources from "@azure/arm-resources";
|
|
11
28
|
import { DefaultAzureCredential } from "@azure/identity";
|
|
12
29
|
import { getLogger } from "@logtape/logtape";
|
|
30
|
+
import pLimit from "p-limit";
|
|
13
31
|
|
|
14
32
|
import type { AzureConfig, AzureDetailedResourceReport } from "./types.js";
|
|
15
33
|
|
|
@@ -19,19 +37,16 @@ import {
|
|
|
19
37
|
mergeResults,
|
|
20
38
|
type Thresholds,
|
|
21
39
|
} from "../types.js";
|
|
22
|
-
import { generateReport } from "./report.js";
|
|
23
40
|
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
} from "./resources/index.js";
|
|
34
|
-
import { matchesTags } from "./utils.js";
|
|
41
|
+
type Analyzer,
|
|
42
|
+
type AnalyzerContext,
|
|
43
|
+
type AzureClients,
|
|
44
|
+
createDefaultAnalyzers,
|
|
45
|
+
} from "./analyzers/index.js";
|
|
46
|
+
import { generateReport } from "./report.js";
|
|
47
|
+
import { matchesTags, type MetricsCache } from "./utils.js";
|
|
48
|
+
|
|
49
|
+
const DEFAULT_CONCURRENCY = 8;
|
|
35
50
|
|
|
36
51
|
/**
|
|
37
52
|
* Analyzes resources in multiple Azure subscriptions and generates a report.
|
|
@@ -47,62 +62,96 @@ export async function analyzeAzureResources(
|
|
|
47
62
|
const credential = new DefaultAzureCredential();
|
|
48
63
|
const allReports: AzureDetailedResourceReport[] = [];
|
|
49
64
|
|
|
65
|
+
const analyzers = createDefaultAnalyzers();
|
|
66
|
+
const thresholds: Thresholds = config.thresholds ?? DEFAULT_THRESHOLDS;
|
|
67
|
+
|
|
68
|
+
// Normalise concurrency the same way p-limit does to keep maxInFlight
|
|
69
|
+
// consistent. A raw value of 0/NaN would produce maxInFlight = 0/NaN and
|
|
70
|
+
// either deadlock or silently disable backpressure.
|
|
71
|
+
const rawConcurrency = config.concurrency ?? DEFAULT_CONCURRENCY;
|
|
72
|
+
const concurrency = Number.isFinite(rawConcurrency)
|
|
73
|
+
? Math.max(1, Math.floor(rawConcurrency))
|
|
74
|
+
: 1;
|
|
75
|
+
const limit = pLimit(concurrency);
|
|
76
|
+
|
|
77
|
+
// Bound the in-flight Set to `2 × concurrency` so memory stays proportional
|
|
78
|
+
// to the limiter width, not the total resource count in a subscription.
|
|
79
|
+
const maxInFlight = concurrency * 2;
|
|
80
|
+
|
|
50
81
|
for (const subscriptionId of config.subscriptionIds) {
|
|
51
82
|
logger.info(`Analyzing subscription: ${subscriptionId}`);
|
|
52
83
|
|
|
84
|
+
const sid = subscriptionId.trim();
|
|
85
|
+
|
|
86
|
+
// Fresh cache per subscription — bounds peak memory to one subscription's
|
|
87
|
+
// worth of metrics and keeps concurrent analyzeAzureResources calls isolated.
|
|
88
|
+
const runCache: MetricsCache = new Map();
|
|
89
|
+
|
|
90
|
+
const clients: AzureClients = {
|
|
91
|
+
compute: new ComputeManagementClient(credential, sid),
|
|
92
|
+
containerApps: new ContainerAppsAPIClient(credential, sid),
|
|
93
|
+
monitor: new MonitorClient(credential, sid),
|
|
94
|
+
network: new NetworkManagementClient(credential, sid),
|
|
95
|
+
webSite: new WebSiteManagementClient(credential, sid),
|
|
96
|
+
};
|
|
53
97
|
const resourceClient = new armResources.ResourceManagementClient(
|
|
54
98
|
credential,
|
|
55
|
-
|
|
56
|
-
);
|
|
57
|
-
const monitorClient = new MonitorClient(credential, subscriptionId.trim());
|
|
58
|
-
const computeClient = new ComputeManagementClient(
|
|
59
|
-
credential,
|
|
60
|
-
subscriptionId.trim(),
|
|
61
|
-
);
|
|
62
|
-
const networkClient = new NetworkManagementClient(
|
|
63
|
-
credential,
|
|
64
|
-
subscriptionId.trim(),
|
|
65
|
-
);
|
|
66
|
-
const webSiteClient = new WebSiteManagementClient(
|
|
67
|
-
credential,
|
|
68
|
-
subscriptionId.trim(),
|
|
69
|
-
);
|
|
70
|
-
const containerAppsClient = new ContainerAppsAPIClient(
|
|
71
|
-
credential,
|
|
72
|
-
subscriptionId.trim(),
|
|
99
|
+
sid,
|
|
73
100
|
);
|
|
74
101
|
|
|
75
|
-
const
|
|
102
|
+
const inFlight = new Set<Promise<void>>();
|
|
76
103
|
|
|
77
|
-
// Use the async iterator to avoid
|
|
104
|
+
// Use the async iterator to avoid loading all resources into memory at once.
|
|
78
105
|
for await (const resource of resourceClient.resources.list()) {
|
|
79
|
-
// Skip resources that don't match the requested tag filter
|
|
80
106
|
if (!matchesTags(resource, config.filterTags)) {
|
|
81
107
|
continue;
|
|
82
108
|
}
|
|
83
109
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
110
|
+
// Backpressure: wait for a slot before enqueuing the next task so that
|
|
111
|
+
// the inFlight Set stays bounded by maxInFlight instead of growing to the
|
|
112
|
+
// total resource count in the subscription.
|
|
113
|
+
while (inFlight.size >= maxInFlight) {
|
|
114
|
+
await Promise.race(inFlight).catch(() => undefined);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const task: Promise<void> = limit(async () => {
|
|
118
|
+
const { costRisk, reason, suspectedUnused } = await analyzeResource(
|
|
119
|
+
resource,
|
|
120
|
+
analyzers,
|
|
121
|
+
clients,
|
|
122
|
+
runCache,
|
|
123
|
+
config.preferredLocation,
|
|
124
|
+
config.timespanDays,
|
|
125
|
+
thresholds,
|
|
126
|
+
config.verbose || false,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
if (suspectedUnused) {
|
|
130
|
+
allReports.push({
|
|
131
|
+
analysis: {
|
|
132
|
+
costRisk,
|
|
133
|
+
reason: reason || "No specific findings.",
|
|
134
|
+
suspectedUnused,
|
|
135
|
+
},
|
|
136
|
+
resource,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
inFlight.add(task);
|
|
142
|
+
// Suppress the unhandled-rejection that would occur between task creation
|
|
143
|
+
// and the Promise.allSettled drain below. The .catch() handler is a no-op
|
|
144
|
+
// because the actual error is still visible to allSettled (which logs it)
|
|
145
|
+
// via the original `task` reference kept in inFlight.
|
|
146
|
+
void task.catch(() => undefined).finally(() => inFlight.delete(task));
|
|
147
|
+
}
|
|
96
148
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
},
|
|
104
|
-
resource: resource,
|
|
105
|
-
});
|
|
149
|
+
// Drain remaining tasks; surface any unexpected errors so they don't
|
|
150
|
+
// disappear silently and produce an incomplete report without a signal.
|
|
151
|
+
const results = await Promise.allSettled(inFlight);
|
|
152
|
+
for (const result of results) {
|
|
153
|
+
if (result.status === "rejected") {
|
|
154
|
+
logger.error(`Resource analysis failed: ${String(result.reason)}`);
|
|
106
155
|
}
|
|
107
156
|
}
|
|
108
157
|
}
|
|
@@ -119,34 +168,32 @@ export async function analyzeAzureResources(
|
|
|
119
168
|
}
|
|
120
169
|
|
|
121
170
|
/**
|
|
122
|
-
* Analyzes a single Azure resource
|
|
171
|
+
* Analyzes a single Azure resource by dispatching it to every registered
|
|
172
|
+
* analyzer that supports it. Generic checks (missing tags, location
|
|
173
|
+
* mismatch) are applied around the analyzer-specific logic.
|
|
123
174
|
*
|
|
124
|
-
* @param resource
|
|
125
|
-
* @param
|
|
126
|
-
* @param
|
|
127
|
-
* @param
|
|
128
|
-
* @param
|
|
129
|
-
* @param
|
|
130
|
-
* @param
|
|
131
|
-
* @param
|
|
132
|
-
* @param verbose - Whether verbose logging is enabled
|
|
175
|
+
* @param resource The Azure resource to analyze
|
|
176
|
+
* @param analyzers Registered analyzers (typically `createDefaultAnalyzers()`)
|
|
177
|
+
* @param clients Bundle of Azure SDK clients shared across analyzers
|
|
178
|
+
* @param metricsCache Run-scoped metrics cache to pass through to analyzers
|
|
179
|
+
* @param preferredLocation Preferred Azure region (resources elsewhere are flagged)
|
|
180
|
+
* @param timespanDays Look-back window for Azure Monitor metrics
|
|
181
|
+
* @param thresholds Numeric thresholds used during analysis
|
|
182
|
+
* @param verbose Whether verbose logging is enabled
|
|
133
183
|
* @returns Analysis result with cost risk and reason
|
|
134
184
|
*/
|
|
135
185
|
export async function analyzeResource(
|
|
136
186
|
resource: armResources.GenericResource,
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
webSiteClient: WebSiteManagementClient,
|
|
141
|
-
containerAppsClient: ContainerAppsAPIClient,
|
|
187
|
+
analyzers: Analyzer[],
|
|
188
|
+
clients: AzureClients,
|
|
189
|
+
metricsCache: MetricsCache = new Map(),
|
|
142
190
|
preferredLocation: string,
|
|
143
191
|
timespanDays: number,
|
|
144
192
|
thresholds: Thresholds,
|
|
145
193
|
verbose = false,
|
|
146
194
|
): Promise<AnalysisResult> {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
costRisk: "low" as "high" | "low" | "medium",
|
|
195
|
+
let result: AnalysisResult = {
|
|
196
|
+
costRisk: "low",
|
|
150
197
|
reason: "",
|
|
151
198
|
suspectedUnused: false,
|
|
152
199
|
};
|
|
@@ -157,100 +204,28 @@ export async function analyzeResource(
|
|
|
157
204
|
result.reason += "No tags found. ";
|
|
158
205
|
}
|
|
159
206
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
case "microsoft.compute/disks": {
|
|
175
|
-
const diskResult = await analyzeDisk(resource, computeClient, verbose);
|
|
176
|
-
result = mergeResults(result, diskResult);
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
case "microsoft.compute/virtualmachines": {
|
|
180
|
-
const vmResult = await analyzeVM(
|
|
181
|
-
resource,
|
|
182
|
-
monitorClient,
|
|
183
|
-
computeClient,
|
|
184
|
-
timespanDays,
|
|
185
|
-
thresholds,
|
|
186
|
-
verbose,
|
|
187
|
-
);
|
|
188
|
-
result = mergeResults(result, vmResult);
|
|
189
|
-
break;
|
|
190
|
-
}
|
|
191
|
-
case "microsoft.network/networkinterfaces": {
|
|
192
|
-
const nicResult = await analyzeNic(resource, networkClient, verbose);
|
|
193
|
-
result = mergeResults(result, nicResult);
|
|
194
|
-
break;
|
|
195
|
-
}
|
|
196
|
-
case "microsoft.network/privateendpoints": {
|
|
197
|
-
const peResult = await analyzePrivateEndpoint(
|
|
198
|
-
resource,
|
|
199
|
-
networkClient,
|
|
200
|
-
verbose,
|
|
201
|
-
);
|
|
202
|
-
result = mergeResults(result, peResult);
|
|
203
|
-
break;
|
|
204
|
-
}
|
|
205
|
-
case "microsoft.network/publicipaddresses": {
|
|
206
|
-
const pipResult = await analyzePublicIp(
|
|
207
|
-
resource,
|
|
208
|
-
networkClient,
|
|
209
|
-
monitorClient,
|
|
210
|
-
timespanDays,
|
|
211
|
-
thresholds,
|
|
212
|
-
verbose,
|
|
213
|
-
);
|
|
214
|
-
result = mergeResults(result, pipResult);
|
|
215
|
-
break;
|
|
216
|
-
}
|
|
217
|
-
case "microsoft.storage/storageaccounts": {
|
|
218
|
-
const storageResult = await analyzeStorageAccount(
|
|
219
|
-
resource,
|
|
220
|
-
monitorClient,
|
|
221
|
-
timespanDays,
|
|
222
|
-
thresholds,
|
|
223
|
-
verbose,
|
|
224
|
-
);
|
|
225
|
-
result = mergeResults(result, storageResult);
|
|
226
|
-
break;
|
|
227
|
-
}
|
|
228
|
-
case "microsoft.web/serverfarms": {
|
|
229
|
-
const aspResult = await analyzeAppServicePlan(
|
|
230
|
-
resource,
|
|
231
|
-
webSiteClient,
|
|
232
|
-
monitorClient,
|
|
233
|
-
timespanDays,
|
|
234
|
-
thresholds,
|
|
235
|
-
verbose,
|
|
236
|
-
);
|
|
237
|
-
result = mergeResults(result, aspResult);
|
|
238
|
-
break;
|
|
239
|
-
}
|
|
240
|
-
case "microsoft.web/staticsites": {
|
|
241
|
-
const staticSiteResult = await analyzeStaticSite(
|
|
242
|
-
resource,
|
|
243
|
-
monitorClient,
|
|
244
|
-
timespanDays,
|
|
245
|
-
thresholds,
|
|
246
|
-
verbose,
|
|
247
|
-
);
|
|
248
|
-
result = mergeResults(result, staticSiteResult);
|
|
249
|
-
break;
|
|
207
|
+
const ctx: AnalyzerContext = {
|
|
208
|
+
clients,
|
|
209
|
+
metricsCache,
|
|
210
|
+
preferredLocation,
|
|
211
|
+
resource,
|
|
212
|
+
thresholds,
|
|
213
|
+
timespanDays,
|
|
214
|
+
verbose,
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
let matched = false;
|
|
218
|
+
for (const analyzer of analyzers) {
|
|
219
|
+
if (!analyzer.supports(resource)) {
|
|
220
|
+
continue;
|
|
250
221
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
222
|
+
matched = true;
|
|
223
|
+
const specific = await analyzer.analyze(ctx);
|
|
224
|
+
result = mergeResults(result, specific);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (!matched) {
|
|
228
|
+
result.reason += "No specific analysis for this resource type. ";
|
|
254
229
|
}
|
|
255
230
|
|
|
256
231
|
// Generic check for location
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default registry of Azure analyzers.
|
|
3
|
+
*
|
|
4
|
+
* Each entry wraps an existing per-type analyzer function (kept in
|
|
5
|
+
* `../resources/`) and exposes it through the unified `Analyzer`
|
|
6
|
+
* interface. The orchestrator simply iterates the registry — no big
|
|
7
|
+
* `switch` statement, no risk of forgetting to wire a new analyzer
|
|
8
|
+
* into the orchestrator when the catalog grows.
|
|
9
|
+
*
|
|
10
|
+
* Adding a new analyzer is a single insertion here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Analyzer } from "./types.js";
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
analyzeAppServicePlan,
|
|
17
|
+
analyzeContainerApp,
|
|
18
|
+
analyzeDisk,
|
|
19
|
+
analyzeNic,
|
|
20
|
+
analyzePrivateEndpoint,
|
|
21
|
+
analyzePublicIp,
|
|
22
|
+
analyzeStaticSite,
|
|
23
|
+
analyzeStorageAccount,
|
|
24
|
+
analyzeVM,
|
|
25
|
+
} from "../resources/index.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Builds the default set of analyzers in the same order they were
|
|
29
|
+
* previously evaluated by the orchestrator's `switch` statement. The
|
|
30
|
+
* order is not behaviourally meaningful today (each resource is matched
|
|
31
|
+
* by exactly one analyzer) but is kept deterministic for predictable
|
|
32
|
+
* logging and to ease future debugging.
|
|
33
|
+
*/
|
|
34
|
+
export function createDefaultAnalyzers(): Analyzer[] {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
analyze: ({
|
|
38
|
+
clients,
|
|
39
|
+
metricsCache,
|
|
40
|
+
resource,
|
|
41
|
+
thresholds,
|
|
42
|
+
timespanDays,
|
|
43
|
+
verbose,
|
|
44
|
+
}) =>
|
|
45
|
+
analyzeContainerApp(
|
|
46
|
+
resource,
|
|
47
|
+
clients.containerApps,
|
|
48
|
+
clients.monitor,
|
|
49
|
+
timespanDays,
|
|
50
|
+
thresholds,
|
|
51
|
+
verbose,
|
|
52
|
+
metricsCache,
|
|
53
|
+
),
|
|
54
|
+
id: "azure.container-app",
|
|
55
|
+
supports: (r) => r.type?.toLowerCase() === "microsoft.app/containerapps",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
analyze: ({ clients, resource, verbose }) =>
|
|
59
|
+
analyzeDisk(resource, clients.compute, verbose),
|
|
60
|
+
id: "azure.disk",
|
|
61
|
+
supports: (r) => r.type?.toLowerCase() === "microsoft.compute/disks",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
analyze: ({
|
|
65
|
+
clients,
|
|
66
|
+
metricsCache,
|
|
67
|
+
resource,
|
|
68
|
+
thresholds,
|
|
69
|
+
timespanDays,
|
|
70
|
+
verbose,
|
|
71
|
+
}) =>
|
|
72
|
+
analyzeVM(
|
|
73
|
+
resource,
|
|
74
|
+
clients.monitor,
|
|
75
|
+
clients.compute,
|
|
76
|
+
timespanDays,
|
|
77
|
+
thresholds,
|
|
78
|
+
verbose,
|
|
79
|
+
metricsCache,
|
|
80
|
+
),
|
|
81
|
+
id: "azure.vm",
|
|
82
|
+
supports: (r) =>
|
|
83
|
+
r.type?.toLowerCase() === "microsoft.compute/virtualmachines",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
analyze: ({ clients, resource, verbose }) =>
|
|
87
|
+
analyzeNic(resource, clients.network, verbose),
|
|
88
|
+
id: "azure.nic",
|
|
89
|
+
supports: (r) =>
|
|
90
|
+
r.type?.toLowerCase() === "microsoft.network/networkinterfaces",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
analyze: ({ clients, resource, verbose }) =>
|
|
94
|
+
analyzePrivateEndpoint(resource, clients.network, verbose),
|
|
95
|
+
id: "azure.private-endpoint",
|
|
96
|
+
supports: (r) =>
|
|
97
|
+
r.type?.toLowerCase() === "microsoft.network/privateendpoints",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
analyze: ({
|
|
101
|
+
clients,
|
|
102
|
+
metricsCache,
|
|
103
|
+
resource,
|
|
104
|
+
thresholds,
|
|
105
|
+
timespanDays,
|
|
106
|
+
verbose,
|
|
107
|
+
}) =>
|
|
108
|
+
analyzePublicIp(
|
|
109
|
+
resource,
|
|
110
|
+
clients.network,
|
|
111
|
+
clients.monitor,
|
|
112
|
+
timespanDays,
|
|
113
|
+
thresholds,
|
|
114
|
+
verbose,
|
|
115
|
+
metricsCache,
|
|
116
|
+
),
|
|
117
|
+
id: "azure.public-ip",
|
|
118
|
+
supports: (r) =>
|
|
119
|
+
r.type?.toLowerCase() === "microsoft.network/publicipaddresses",
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
analyze: ({
|
|
123
|
+
clients,
|
|
124
|
+
metricsCache,
|
|
125
|
+
resource,
|
|
126
|
+
thresholds,
|
|
127
|
+
timespanDays,
|
|
128
|
+
verbose,
|
|
129
|
+
}) =>
|
|
130
|
+
analyzeStorageAccount(
|
|
131
|
+
resource,
|
|
132
|
+
clients.monitor,
|
|
133
|
+
timespanDays,
|
|
134
|
+
thresholds,
|
|
135
|
+
verbose,
|
|
136
|
+
metricsCache,
|
|
137
|
+
),
|
|
138
|
+
id: "azure.storage-account",
|
|
139
|
+
supports: (r) =>
|
|
140
|
+
r.type?.toLowerCase() === "microsoft.storage/storageaccounts",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
analyze: ({
|
|
144
|
+
clients,
|
|
145
|
+
metricsCache,
|
|
146
|
+
resource,
|
|
147
|
+
thresholds,
|
|
148
|
+
timespanDays,
|
|
149
|
+
verbose,
|
|
150
|
+
}) =>
|
|
151
|
+
analyzeAppServicePlan(
|
|
152
|
+
resource,
|
|
153
|
+
clients.webSite,
|
|
154
|
+
clients.monitor,
|
|
155
|
+
timespanDays,
|
|
156
|
+
thresholds,
|
|
157
|
+
verbose,
|
|
158
|
+
metricsCache,
|
|
159
|
+
),
|
|
160
|
+
id: "azure.app-service-plan",
|
|
161
|
+
supports: (r) => r.type?.toLowerCase() === "microsoft.web/serverfarms",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
analyze: ({
|
|
165
|
+
clients,
|
|
166
|
+
metricsCache,
|
|
167
|
+
resource,
|
|
168
|
+
thresholds,
|
|
169
|
+
timespanDays,
|
|
170
|
+
verbose,
|
|
171
|
+
}) =>
|
|
172
|
+
analyzeStaticSite(
|
|
173
|
+
resource,
|
|
174
|
+
clients.monitor,
|
|
175
|
+
timespanDays,
|
|
176
|
+
thresholds,
|
|
177
|
+
verbose,
|
|
178
|
+
metricsCache,
|
|
179
|
+
),
|
|
180
|
+
id: "azure.static-web-app",
|
|
181
|
+
supports: (r) => r.type?.toLowerCase() === "microsoft.web/staticsites",
|
|
182
|
+
},
|
|
183
|
+
];
|
|
184
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin architecture for Azure resource analyzers.
|
|
3
|
+
*
|
|
4
|
+
* Each `Analyzer` is a self-contained unit that:
|
|
5
|
+
* 1. declares the resource types it can handle (`supports`)
|
|
6
|
+
* 2. produces an `AnalysisResult` for a given resource (`analyze`)
|
|
7
|
+
*
|
|
8
|
+
* The orchestrator in `analyzer.ts` walks the registered analyzers for
|
|
9
|
+
* every resource it encounters. New sources (Azure Advisor, custom
|
|
10
|
+
* checks, pricing-enriched analyzers, …) can be added by implementing
|
|
11
|
+
* the interface and registering them in `registry.ts` without touching
|
|
12
|
+
* the orchestrator.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ContainerAppsAPIClient } from "@azure/arm-appcontainers";
|
|
16
|
+
import type { WebSiteManagementClient } from "@azure/arm-appservice";
|
|
17
|
+
import type { ComputeManagementClient } from "@azure/arm-compute";
|
|
18
|
+
import type { MonitorClient } from "@azure/arm-monitor";
|
|
19
|
+
import type { NetworkManagementClient } from "@azure/arm-network";
|
|
20
|
+
import type * as armResources from "@azure/arm-resources";
|
|
21
|
+
|
|
22
|
+
import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
23
|
+
import type { MetricsCache } from "../utils.js";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Contract every analyzer must satisfy.
|
|
27
|
+
*/
|
|
28
|
+
export type Analyzer = {
|
|
29
|
+
analyze(ctx: AnalyzerContext): Promise<AnalysisResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Stable identifier of the analyzer (e.g. `azure.vm`, `azure.advisor`).
|
|
32
|
+
* Used for logging, telemetry and future deduplication logic.
|
|
33
|
+
*/
|
|
34
|
+
readonly id: string;
|
|
35
|
+
supports(resource: armResources.GenericResource): boolean;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Per-resource analysis context handed to every analyzer.
|
|
40
|
+
*/
|
|
41
|
+
export type AnalyzerContext = {
|
|
42
|
+
clients: AzureClients;
|
|
43
|
+
/**
|
|
44
|
+
* Run-scoped metrics cache. Pass through to `getMetric` calls so that
|
|
45
|
+
* concurrent `analyzeAzureResources` invocations stay isolated.
|
|
46
|
+
*/
|
|
47
|
+
metricsCache: MetricsCache;
|
|
48
|
+
preferredLocation: string;
|
|
49
|
+
resource: armResources.GenericResource;
|
|
50
|
+
thresholds: Thresholds;
|
|
51
|
+
timespanDays: number;
|
|
52
|
+
verbose: boolean;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Bundle of Azure SDK clients an analyzer might need. The orchestrator
|
|
57
|
+
* builds them once per subscription and passes the same instances to
|
|
58
|
+
* every analyzer.
|
|
59
|
+
*/
|
|
60
|
+
export type AzureClients = {
|
|
61
|
+
compute: ComputeManagementClient;
|
|
62
|
+
containerApps: ContainerAppsAPIClient;
|
|
63
|
+
monitor: MonitorClient;
|
|
64
|
+
network: NetworkManagementClient;
|
|
65
|
+
webSite: WebSiteManagementClient;
|
|
66
|
+
};
|
package/src/azure/config.ts
CHANGED
|
@@ -42,6 +42,7 @@ export async function loadAzureConfig(
|
|
|
42
42
|
const rawYaml = yaml.load(raw);
|
|
43
43
|
const parsed = ConfigSchema.parse(rawYaml);
|
|
44
44
|
return {
|
|
45
|
+
concurrency: parsed.azure.concurrency,
|
|
45
46
|
preferredLocation: parsed.azure.preferredLocation,
|
|
46
47
|
subscriptionIds: parsed.azure.subscriptionIds,
|
|
47
48
|
thresholds: parsed.azure.thresholds,
|
|
@@ -13,6 +13,7 @@ import type { AnalysisResult, Thresholds } from "../../types.js";
|
|
|
13
13
|
import { DEFAULT_THRESHOLDS } from "../../types.js";
|
|
14
14
|
import {
|
|
15
15
|
getMetric,
|
|
16
|
+
type MetricsCache,
|
|
16
17
|
verboseLog,
|
|
17
18
|
verboseLogAnalysisResult,
|
|
18
19
|
verboseLogResourceStart,
|
|
@@ -34,6 +35,7 @@ export async function analyzeAppServicePlan(
|
|
|
34
35
|
timespanDays: number,
|
|
35
36
|
thresholds: Thresholds = DEFAULT_THRESHOLDS,
|
|
36
37
|
verbose = false,
|
|
38
|
+
cache?: MetricsCache,
|
|
37
39
|
): Promise<AnalysisResult> {
|
|
38
40
|
verboseLogResourceStart(
|
|
39
41
|
verbose,
|
|
@@ -79,6 +81,7 @@ export async function analyzeAppServicePlan(
|
|
|
79
81
|
"CpuPercentage",
|
|
80
82
|
"Average",
|
|
81
83
|
timespanDays,
|
|
84
|
+
cache,
|
|
82
85
|
);
|
|
83
86
|
|
|
84
87
|
const memoryPercentage = await getMetric(
|
|
@@ -87,6 +90,7 @@ export async function analyzeAppServicePlan(
|
|
|
87
90
|
"MemoryPercentage",
|
|
88
91
|
"Average",
|
|
89
92
|
timespanDays,
|
|
93
|
+
cache,
|
|
90
94
|
);
|
|
91
95
|
|
|
92
96
|
if (
|