feat: implement VM SKU chooser tool with filtering and location persistence

This commit is contained in:
2026-04-20 18:14:32 +02:00
parent 5304b07d17
commit cc4ff948c5
12 changed files with 606 additions and 8 deletions

View File

@@ -2,7 +2,7 @@ import { ComputeManagementClient } from "@azure/arm-compute";
import { DefaultAzureCredential } from "@azure/identity";
import { MemoryCache } from "./cache";
import { sortImageVersionsIfSemantic } from "./version";
import type { LocationOption } from "./types";
import type { LocationOption, VmSkuOption } from "./types";
const CACHE_TTL_MS = 5 * 60 * 1000;
@@ -116,6 +116,47 @@ export class AzureImageService {
return sorted;
}
public async getVmSkus(location: string): Promise<VmSkuOption[]> {
const cacheKey = `vm-skus:${location}`;
const cached = this.cache.get<VmSkuOption[]>(cacheKey);
if (cached) {
return cached;
}
const targetLocation = location.toLowerCase();
const vmSkus: VmSkuOption[] = [];
for await (const sku of this.computeClient.resourceSkus.list()) {
if ((sku.resourceType ?? "").toLowerCase() !== "virtualmachines") {
continue;
}
const skuLocations = (sku.locations ?? []).map((item) => item.toLowerCase());
const matchesLocation = skuLocations.includes(targetLocation);
if (!matchesLocation) {
continue;
}
const capabilities = new Map(
(sku.capabilities ?? []).map((item) => [item.name?.toLowerCase() ?? "", item.value ?? ""])
);
vmSkus.push({
name: sku.name ?? "",
size: sku.size ?? "",
family: sku.family ?? "",
tier: sku.tier ?? "",
vcpus: this.toNumber(capabilities.get("vcpus")),
memoryGb: this.toNumber(capabilities.get("memorygb")),
maxDataDiskCount: this.toNumber(capabilities.get("maxdatadiskcount"))
});
}
vmSkus.sort((a, b) => a.name.localeCompare(b.name));
this.cache.set(cacheKey, vmSkus, CACHE_TTL_MS);
return vmSkus;
}
private extractNames(source: unknown): string[] {
const items = Array.isArray(source)
? source
@@ -127,4 +168,14 @@ export class AzureImageService {
.map((item) => (typeof item === "object" && item !== null && "name" in item ? (item as { name?: string }).name : undefined))
.filter((value): value is string => Boolean(value));
}
private toNumber(value: string | undefined): number | null {
if (!value) {
return null;
}
const parsed = Number.parseFloat(value);
return Number.isFinite(parsed) ? parsed : null;
}
}

View File

@@ -110,6 +110,15 @@ const makeApp = () => {
}
});
app.get("/api/vm-skus", async (req, res, next) => {
try {
const { location } = queryLocation.parse(req.query);
res.json(await requireAzure().getVmSkus(location));
} catch (error) {
next(error);
}
});
app.get("/api/templates", (_req, res) => {
res.json(templates.getTemplates());
});

View File

@@ -16,3 +16,13 @@ export type ImageSelection = {
sku: string;
version: string;
};
export type VmSkuOption = {
name: string;
size: string;
family: string;
tier: string;
vcpus: number | null;
memoryGb: number | null;
maxDataDiskCount: number | null;
};