feat: implement VM SKU chooser tool with filtering and location persistence
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user