Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: fix
packages:
- "@azure-tools/typespec-azure-resource-manager"
---

Fix `resolveArmResources` incorrectly merging cross-scope `LegacyOperations` into a single resource. Operations at different scopes (e.g., subscription vs tenant) with the same model but no explicit resource name are now resolved as separate resources.
56 changes: 45 additions & 11 deletions packages/typespec-azure-resource-manager/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ export interface ResolvedResourceInfo {
resourceInstancePath: string;
/** The name of the resource at this instance path */
resourceName: string;
/** Whether the resource name was explicitly provided as a parameter */
resourceNameIsExplicit?: boolean;
}

interface ResolvedResourceOperations {
Expand All @@ -165,6 +167,8 @@ interface ResolvedResourceOperations {
associatedOperations?: ArmResourceOperation[];
/** The name of the resource at this instance path */
resourceName: string;
/** Whether the resource name was explicitly provided as a parameter */
resourceNameIsExplicit?: boolean;
/** The resource type (The actual resource type string will be "${provider}/${types.join("/")}) */
resourceType: ResourceType;
/** The path to the instance of a resource */
Expand Down Expand Up @@ -631,6 +635,30 @@ function isVariableSegment(segment: string): boolean {
return (segment.startsWith("{") && segment.endsWith("}")) || segment === "default";
}

/**
* Extracts the scope prefix from a resource instance path.
* The scope prefix is the portion of the path before the last `/providers/` occurrence.
* For example:
* - `/subscriptions/{id}/providers/Microsoft.Foo/bars/{name}` → `/subscriptions/{id}`
* - `/providers/Microsoft.Foo/bars/{name}` → `` (empty)
*/
function getScopePrefix(resourceInstancePath: string): string {
const lastProviders = resourceInstancePath.lastIndexOf("/providers/");
if (lastProviders <= 0) return "";
return resourceInstancePath.slice(0, lastProviders);
}

/**
* Normalizes a path for scope comparison by lowercasing static segments
* and replacing variable segments with a placeholder.
*/
function normalizePathForScopeComparison(path: string): string {
return path
.split("/")
.map((s) => (isVariableSegment(s) ? "{}" : s.toLowerCase()))
.join("/");
}

function getResourceInfo(
program: Program,
operation: ArmResourceOperation,
Expand Down Expand Up @@ -744,11 +772,13 @@ export function isResourceOperationMatch(
resourceType: ResourceType;
resourceInstancePath: string;
resourceName?: string;
resourceNameIsExplicit?: boolean;
},
target: {
resourceType: ResourceType;
resourceInstancePath: string;
resourceName?: string;
resourceNameIsExplicit?: boolean;
},
): boolean {
if (
Expand All @@ -764,17 +794,18 @@ export function isResourceOperationMatch(
if (source.resourceType.types[i].toLowerCase() !== target.resourceType.types[i].toLowerCase())
return false;
}
/*const sourceSegments = source.resourceInstancePath.split("/");
const targetSegments = target.resourceInstancePath.split("/");
if (sourceSegments.length !== targetSegments.length) return false;
for (let i = 0; i < sourceSegments.length; i++) {
if (!isVariableSegment(sourceSegments[i])) {
if (isVariableSegment(targetSegments[i])) {
return false;
}
if (sourceSegments[i].toLowerCase() !== targetSegments[i].toLowerCase()) return false;
} else if (!isVariableSegment(targetSegments[i])) return false;
}*/

// When neither resource has an explicitly provided resource name, also compare
// the scope prefix of the instance path to prevent merging cross-scope operations
if (!source.resourceNameIsExplicit && !target.resourceNameIsExplicit) {
const sourceScope = getScopePrefix(source.resourceInstancePath);
const targetScope = getScopePrefix(target.resourceInstancePath);
if (
normalizePathForScopeComparison(sourceScope) !== normalizePathForScopeComparison(targetScope)
)
return false;
}

return true;
}

Expand Down Expand Up @@ -874,10 +905,12 @@ export function resolveArmResourceOperations(
if (resourceInfo === undefined) continue;
armOperation.name = operation.name;
armOperation.resourceKind = operation.resourceKind;
const resourceNameIsExplicit = operation.resourceName !== undefined;
resourceInfo.resourceName =
operation.resourceName ??
getResourceNameForOperation(program, armOperation, resourceInfo.resourceInstancePath) ??
armOperation.resourceModelName;
resourceInfo.resourceNameIsExplicit = resourceNameIsExplicit;
armOperation.resourceName = resourceInfo.resourceName;

let matched = false;
Expand All @@ -899,6 +932,7 @@ export function resolveArmResourceOperations(
resourceType: resourceInfo.resourceType,
resourceInstancePath: resourceInfo.resourceInstancePath,
resourceName: resourceInfo.resourceName,
resourceNameIsExplicit: resourceNameIsExplicit,
operations: {
lifecycle: {
read: undefined,
Expand Down
Loading
Loading