Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 119 additions & 2 deletions packages/typespec-azure-resource-manager/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -1229,6 +1238,114 @@ export function resolveArmResourceOperations(
});
}

function disambiguateImplicitResourceNameCollisions(resources: ResolvedResourceOperations[]): void {
const resourcesByName = new Map<string, ResolvedResourceOperations[]>();
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<string, ResolvedResourceOperations[]>();

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>): 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VirtualMachineProperties> {
...ResourceNameParameter<
Resource = VirtualMachine,
KeyName = "vmName",
SegmentName = "virtualMachines",
NamePattern = ""
>;
}

model VirtualMachineScaleSetProperties {}

model VirtualMachineScaleSet is TrackedResource<VirtualMachineScaleSetProperties> {
...ResourceNameParameter<
Resource = VirtualMachineScaleSet,
KeyName = "vmScaleSetName",
SegmentName = "virtualMachineScaleSets",
NamePattern = ""
>;
}

model VirtualMachineScaleSetVMProperties {}

@parentResource(VirtualMachineScaleSet)
model VirtualMachineScaleSetVM is ProxyResource<VirtualMachineScaleSetVMProperties> {
...ResourceNameParameter<
Resource = VirtualMachineScaleSetVM,
KeyName = "instanceId",
SegmentName = "virtualMachines",
NamePattern = ""
>;
}

model VirtualMachineRunCommandProperties {}

model CloudError {}

@parentResource(VirtualMachine)
model VirtualMachineRunCommand is TrackedResource<VirtualMachineRunCommandProperties> {
...ResourceNameParameter<
Resource = VirtualMachineRunCommand,
KeyName = "runCommandName",
SegmentName = "runCommands",
NamePattern = ""
>;
}

interface Operations extends Azure.ResourceManager.Operations {}

alias VirtualMachinePath = {
...ApiVersionParameter;
...SubscriptionIdParameter;
...ResourceGroupParameter;
...Azure.ResourceManager.Legacy.Provider;
...KeysOf<ResourceNameParameter<
VirtualMachine,
SegmentName = "virtualMachines",
KeyName = "vmName",
NamePattern = ""
>>;
};

@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<ResourceNameParameter<
VirtualMachineScaleSet,
SegmentName = "virtualMachineScaleSets",
KeyName = "vmScaleSetName",
NamePattern = ""
>>;
...KeysOf<ResourceNameParameter<
VirtualMachineScaleSetVM,
SegmentName = "virtualMachines",
KeyName = "instanceId",
NamePattern = ""
>>;
};

@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<VirtualMachineRunCommand>;
}

@armResourceOperations
interface VirtualMachineScaleSetVMRunCommands {
get is VirtualMachineScaleSetVMRunCommandOps.Read<VirtualMachineRunCommand>;
}
`);
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" },
Expand Down
Loading