import { ComputeManagementClient } from "@azure/arm-compute"; import { DefaultAzureCredential } from "@azure/identity"; import { MemoryCache } from "./cache"; import { sortImageVersionsIfSemantic } from "./version"; import type { LocationOption } from "./types"; const CACHE_TTL_MS = 5 * 60 * 1000; export class AzureImageService { private readonly credential = new DefaultAzureCredential(); private readonly computeClient: ComputeManagementClient; private readonly cache = new MemoryCache(); public constructor(private readonly subscriptionId: string) { this.computeClient = new ComputeManagementClient(this.credential, subscriptionId); } public async getLocations(): Promise { const cacheKey = "locations"; const cached = this.cache.get(cacheKey); if (cached) { return cached; } const token = await this.credential.getToken("https://management.azure.com/.default"); const url = `https://management.azure.com/subscriptions/${this.subscriptionId}/locations?api-version=2022-12-01`; const response = await fetch(url, { headers: { Authorization: `Bearer ${token?.token ?? ""}` } }); const payload = (await response.json()) as { value?: Array<{ name?: string; displayName?: string; metadata?: { regionType?: string } }> }; const locations = (payload.value ?? []) .filter((loc) => loc.metadata?.regionType === "Physical" && Boolean(loc.name)) .map((loc) => ({ name: loc.name as string, displayName: loc.displayName ?? (loc.name as string) })); locations.sort((a, b) => a.name.localeCompare(b.name)); this.cache.set(cacheKey, locations, CACHE_TTL_MS); return locations; } public async getPublishers(location: string): Promise { const cacheKey = `publishers:${location}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } const response = await this.computeClient.virtualMachineImages.listPublishers(location); const publishers = this.extractNames(response); this.cache.set(cacheKey, publishers, CACHE_TTL_MS); return publishers; } public async getOffers(location: string, publisher: string): Promise { const cacheKey = `offers:${location}:${publisher}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } const response = await this.computeClient.virtualMachineImages.listOffers(location, publisher); const offers = this.extractNames(response); this.cache.set(cacheKey, offers, CACHE_TTL_MS); return offers; } public async getSkus(location: string, publisher: string, offer: string): Promise { const cacheKey = `skus:${location}:${publisher}:${offer}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } const response = await this.computeClient.virtualMachineImages.listSkus(location, publisher, offer); const skus = this.extractNames(response); this.cache.set(cacheKey, skus, CACHE_TTL_MS); return skus; } public async getVersions(location: string, publisher: string, offer: string, sku: string): Promise { const cacheKey = `versions:${location}:${publisher}:${offer}:${sku}`; const cached = this.cache.get(cacheKey); if (cached) { return cached; } const response = await this.computeClient.virtualMachineImages.list(location, publisher, offer, sku); const versions = this.extractNames(response); const sorted = sortImageVersionsIfSemantic(versions); this.cache.set(cacheKey, sorted, CACHE_TTL_MS); return sorted; } private extractNames(source: unknown): string[] { const items = Array.isArray(source) ? source : typeof source === "object" && source !== null && "value" in source && Array.isArray((source as { value?: unknown }).value) ? ((source as { value: unknown[] }).value as unknown[]) : []; return items .map((item) => (typeof item === "object" && item !== null && "name" in item ? (item as { name?: string }).name : undefined)) .filter((value): value is string => Boolean(value)); } }