diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index bed38c7cd2..aac8582b2f 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -75,7 +75,13 @@ import { getArmResource, listArmResources, registerArmResource } from "./private import { ArmStateKeys } from "./state.js"; export type ArmResourceKind = - "Tracked" | "Proxy" | "Extension" | "Virtual" | "Custom" | "BuiltIn" | "Generic"; + | "Tracked" + | "Proxy" + | "Extension" + | "Virtual" + | "Custom" + | "BuiltIn" + | "Generic"; /** * The base details for all kinds of resources @@ -1215,7 +1221,10 @@ export function resolveArmResourceOperations( } } - return [...resolvedOperations.values()].toSorted((a, b) => { + const resolved = [...resolvedOperations.values()]; + disambiguateImplicitResourceNameCollisions(resolved); + + return resolved.toSorted((a, b) => { // Sort by provider, type, then instance path if (a.resourceType.types.length < b.resourceType.types.length) return -1; if (a.resourceType.types.length > b.resourceType.types.length) return 1; @@ -1229,6 +1238,114 @@ export function resolveArmResourceOperations( }); } +function disambiguateImplicitResourceNameCollisions(resources: ResolvedResourceOperations[]): void { + const resourcesByName = new Map(); + for (const resource of resources) { + const key = resource.resourceName.toLowerCase(); + const group = resourcesByName.get(key); + if (group) { + group.push(resource); + } else { + resourcesByName.set(key, [resource]); + } + } + + for (const group of resourcesByName.values()) { + if (group.length < 2) continue; + + const usedNames = new Set( + group + .filter((resource) => resource.resourceNameIsExplicit) + .map((resource) => resource.resourceName.toLowerCase()), + ); + const implicitResources = group.filter((resource) => !resource.resourceNameIsExplicit); + const implicitResourcesByFullName = new Map(); + + for (const resource of implicitResources) { + const fullName = getFullResourceTypeName(resource); + const fullNameKey = fullName.toLowerCase(); + const fullNameGroup = implicitResourcesByFullName.get(fullNameKey); + if (fullNameGroup) { + fullNameGroup.push(resource); + } else { + implicitResourcesByFullName.set(fullNameKey, [resource]); + } + } + + for (const sameFullNameResources of implicitResourcesByFullName.values()) { + if (sameFullNameResources.length === 1) { + const resource = sameFullNameResources[0]; + setResolvedResourceName( + resource, + getUniqueResourceName(getFullResourceTypeName(resource), usedNames), + ); + } else { + for (const resource of sameFullNameResources) { + setResolvedResourceName( + resource, + getUniqueResourceName(getScopedResourceTypeName(resource), usedNames), + ); + } + } + } + } +} + +function getFullResourceTypeName(resource: ResolvedResourceOperations): string { + return resource.resourceType.types.map((type) => pascalCase(type)).join(""); +} + +function getScopedResourceTypeName(resource: ResolvedResourceOperations): string { + const fullName = getFullResourceTypeName(resource); + const scopeName = getScopeName(resource.resourceInstancePath); + return `${scopeName}${fullName}`; +} + +function getScopeName(path: string): string { + if (path.startsWith("/providers/")) return "Tenant"; + if (path.startsWith("/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/")) { + return "ResourceGroup"; + } + if (path.startsWith("/subscriptions/{subscriptionId}/providers/")) return "Subscription"; + if (path.startsWith("/providers/Microsoft.Management/managementGroups/{managementGroupName}/")) { + return "ManagementGroup"; + } + return "Scoped"; +} + +function getUniqueResourceName(name: string, usedNames: Set): string { + let candidate = name; + let suffix = 2; + while (usedNames.has(candidate.toLowerCase())) { + candidate = `${name}${suffix}`; + suffix++; + } + usedNames.add(candidate.toLowerCase()); + return candidate; +} + +function setResolvedResourceName(resource: ResolvedResourceOperations, name: string): void { + resource.resourceName = name; + for (const operation of getResolvedResourceOperations(resource)) { + operation.resourceName = name; + } +} + +function getResolvedResourceOperations( + resource: ResolvedResourceOperations, +): ArmResourceOperation[] { + return [ + ...(resource.operations.lifecycle.read ?? []), + ...(resource.operations.lifecycle.createOrUpdate ?? []), + ...(resource.operations.lifecycle.update ?? []), + ...(resource.operations.lifecycle.delete ?? []), + ...(resource.operations.lifecycle.checkExistence ?? []), + ...(resource.operations.lists ?? []), + ...(resource.operations.actions ?? []), + ...(resource.associatedOperations ?? []), + ]; +} + interface ArmResourceOperationCandidate { armOperation: ArmResourceOperation; resourceInfo?: ResolvedResourceInfo; diff --git a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts index 79c9185130..61e1c4716e 100644 --- a/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource-resolution.test.ts @@ -4317,6 +4317,201 @@ interface SupportTicketsNoSubscription { expect(tenantResource.operations.lists).toHaveLength(1); }); + it("separates Compute run command resources that reuse the same model across different parent paths", async () => { + const { program } = await Tester.compile(` + +using Azure.Core; + +@armProviderNamespace +@service(#{ title: "ComputeManagementClient" }) +@versioned(Versions) +namespace Microsoft.Compute; + +enum Versions { + @armCommonTypesVersion(Azure.ResourceManager.CommonTypes.Versions.v5) + v2024_11_01: "2024-11-01", +} + +model VirtualMachineProperties {} + +model VirtualMachine is TrackedResource { + ...ResourceNameParameter< + Resource = VirtualMachine, + KeyName = "vmName", + SegmentName = "virtualMachines", + NamePattern = "" + >; +} + +model VirtualMachineScaleSetProperties {} + +model VirtualMachineScaleSet is TrackedResource { + ...ResourceNameParameter< + Resource = VirtualMachineScaleSet, + KeyName = "vmScaleSetName", + SegmentName = "virtualMachineScaleSets", + NamePattern = "" + >; +} + +model VirtualMachineScaleSetVMProperties {} + +@parentResource(VirtualMachineScaleSet) +model VirtualMachineScaleSetVM is ProxyResource { + ...ResourceNameParameter< + Resource = VirtualMachineScaleSetVM, + KeyName = "instanceId", + SegmentName = "virtualMachines", + NamePattern = "" + >; +} + +model VirtualMachineRunCommandProperties {} + +model CloudError {} + +@parentResource(VirtualMachine) +model VirtualMachineRunCommand is TrackedResource { + ...ResourceNameParameter< + Resource = VirtualMachineRunCommand, + KeyName = "runCommandName", + SegmentName = "runCommands", + NamePattern = "" + >; +} + +interface Operations extends Azure.ResourceManager.Operations {} + +alias VirtualMachinePath = { + ...ApiVersionParameter; + ...SubscriptionIdParameter; + ...ResourceGroupParameter; + ...Azure.ResourceManager.Legacy.Provider; + ...KeysOf>; +}; + +@armResourceOperations +interface VirtualMachineRunCommandOps + extends Azure.ResourceManager.Legacy.LegacyOperations< + VirtualMachinePath, + KeysOf<{ + ...ResourceNameParameter< + VirtualMachineRunCommand, + SegmentName = "runCommands", + KeyName = "runCommandName", + NamePattern = "" + >, + }>, + CloudError + > {} + +alias VirtualMachineScaleSetVMPath = { + ...ApiVersionParameter; + ...SubscriptionIdParameter; + ...ResourceGroupParameter; + ...Azure.ResourceManager.Legacy.Provider; + ...KeysOf>; + ...KeysOf>; +}; + +@armResourceOperations +interface VirtualMachineScaleSetVMRunCommandOps + extends Azure.ResourceManager.Legacy.LegacyOperations< + VirtualMachineScaleSetVMPath, + KeysOf<{ + ...ResourceNameParameter< + VirtualMachineRunCommand, + SegmentName = "runCommands", + KeyName = "runCommandName", + NamePattern = "" + >, + }>, + CloudError + > {} + +@armResourceOperations +interface VirtualMachineRunCommands { + getByVirtualMachine is VirtualMachineRunCommandOps.Read; +} + +@armResourceOperations +interface VirtualMachineScaleSetVMRunCommands { + get is VirtualMachineScaleSetVMRunCommandOps.Read; +} +`); + const provider = resolveArmResources(program); + expect(provider).toBeDefined(); + expect(provider.resources).toBeDefined(); + ok(provider.resources); + + const virtualMachineRunCommand = provider.resources.find( + (r) => + r.resourceInstancePath === + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}/runCommands/{runCommandName}", + ); + ok(virtualMachineRunCommand); + expect(virtualMachineRunCommand.resourceName).toEqual("VirtualMachinesRunCommands"); + checkResolvedOperations(virtualMachineRunCommand, { + operations: { + lifecycle: { + read: [ + { + operationGroup: "VirtualMachineRunCommands", + name: "getByVirtualMachine", + kind: "read", + }, + ], + }, + }, + resourceType: { + provider: "Microsoft.Compute", + types: ["virtualMachines", "runCommands"], + }, + resourceInstancePath: + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachines/{vmName}/runCommands/{runCommandName}", + }); + + const virtualMachineScaleSetVMRunCommand = provider.resources.find( + (r) => + r.resourceInstancePath === + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}/virtualMachines/{instanceId}/runCommands/{runCommandName}", + ); + ok(virtualMachineScaleSetVMRunCommand); + expect(virtualMachineScaleSetVMRunCommand.resourceName).toEqual( + "VirtualMachineScaleSetsVirtualMachinesRunCommands", + ); + checkResolvedOperations(virtualMachineScaleSetVMRunCommand, { + operations: { + lifecycle: { + read: [ + { operationGroup: "VirtualMachineScaleSetVMRunCommands", name: "get", kind: "read" }, + ], + }, + }, + resourceType: { + provider: "Microsoft.Compute", + types: ["virtualMachineScaleSets", "virtualMachines", "runCommands"], + }, + resourceInstancePath: + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Compute/virtualMachineScaleSets/{vmScaleSetName}/virtualMachines/{instanceId}/runCommands/{runCommandName}", + }); + }, 30_000); + it.each([ { propertyType: "{}" }, { propertyType: "unknown" },