UNPKG

@azure-tools/typespec-azure-resource-manager

Version:

TypeSpec Azure Resource Manager library

759 lines 30.9 kB
import { getAllProperties } from "@azure-tools/typespec-azure-core"; import { $tag, getProperty as compilerGetProperty, getKeyName, getTags, isArrayModelType, isGlobalNamespace, isNeverType, isTemplateDeclaration, isTemplateDeclarationOrInstance, isTemplateInstance, } from "@typespec/compiler"; import { useStateMap } from "@typespec/compiler/utils"; import { getHttpOperation, isPathParam } from "@typespec/http"; import { $autoRoute, getParentResource, getSegment } from "@typespec/rest"; import { reportDiagnostic } from "./lib.js"; import { getArmProviderNamespace, isArmLibraryNamespace, resolveProviderNamespace, } from "./namespace.js"; import { getArmResourceOperationData, getArmResourceOperationList, resolveResourceOperations, } from "./operations.js"; import { getArmResource, listArmResources, registerArmResource } from "./private.decorators.js"; import { ArmStateKeys } from "./state.js"; /** * Marks the given resource as an external resource * @param context The decorator context * @param entity The resource model * @param propertiesType The type of the resource properties */ export const $armVirtualResource = (context, entity, provider = undefined) => { const { program } = context; if (isTemplateDeclaration(entity)) return; const result = { kind: "Virtual", provider, }; program.stateMap(ArmStateKeys.armBuiltInResource).set(entity, result); const pathProperty = getProperty(entity, (p) => isPathParam(program, p) && getSegment(program, p) !== undefined); if (pathProperty === undefined) { reportDiagnostic(program, { code: "resource-without-path-and-segment", target: entity, }); return; } const collectionName = getSegment(program, pathProperty); const keyName = getKeyName(program, pathProperty); if (collectionName === undefined || keyName === undefined) { reportDiagnostic(program, { code: "resource-without-path-and-segment", target: entity, }); return; } registerArmResource(context, entity); }; export const $customAzureResource = (context, entity) => { const { program } = context; if (isTemplateDeclaration(entity)) return; program.stateMap(ArmStateKeys.customAzureResource).set(entity, "Custom"); }; function getProperty(target, predicate) { for (const prop of getAllProperties(target).values()) { if (predicate(prop)) return prop; } return undefined; } /** * Determine if the given model is an external resource. * @param program The program to process. * @param target The model to check. * @returns true if the model or any model it extends is marked as a resource, otherwise false. */ export function isArmVirtualResource(program, target) { return getArmVirtualResourceDetails(program, target) !== undefined; } /** * * @param program The program to process. * @param target The model to get details for * @returns The resource details if the model is an external resource, otherwise undefined. */ export function getArmVirtualResourceDetails(program, target, visited = new Set()) { if (visited.has(target)) return undefined; visited.add(target); if (program.stateMap(ArmStateKeys.armBuiltInResource).has(target)) { return program .stateMap(ArmStateKeys.armBuiltInResource) .get(target); } if (target.baseModel) { const details = getArmVirtualResourceDetails(program, target.baseModel, visited); if (details) return details; } const parent = getParentResource(program, target); if (parent) { return getArmVirtualResourceDetails(program, parent, visited); } return undefined; } /** * Determine if the given model is a custom resource. * @param program The program to process. * @param target The model to check. * @returns true if the model or any model it extends is marked as a resource, otherwise false. */ export function isCustomAzureResource(program, target) { if (program.stateMap(ArmStateKeys.customAzureResource).has(target)) return true; if (target.baseModel) return isCustomAzureResource(program, target.baseModel); return false; } function getArmResourceItemPath(operations) { const returnPath = operations.lifecycle.read?.path || operations.lifecycle.createOrUpdate?.path || operations.lifecycle.delete?.path; if (returnPath !== undefined) return returnPath; const actions = Object.values(operations.actions); if (actions.length > 0) { const longPath = actions[0].path; return longPath.substring(0, longPath.lastIndexOf("/")); } return undefined; } function resolveArmResourceDetails(program, resource) { // Combine fully-resolved operation details with the base details we already have const operations = resolveResourceOperations(program, resource.typespecType); // Calculate the resource type path from the itemPath // TODO: This is currently a problem! We don't have a canonical path to use for the itemPath const itemPath = getArmResourceItemPath(operations); const baseType = getResourceBaseType(program, resource.typespecType); const resourceTypePath = getResourceTypePath(resource, itemPath, baseType); return { ...resource, operations, resourceTypePath, }; } function getResourceTypePath(resource, itemPath, baseType) { if (!itemPath) { return undefined; } // Don't calculate resourceTypePath for tenant-level or extension resources if (baseType === ResourceBaseType.Tenant || baseType === ResourceBaseType.Extension) { return undefined; } // For other resources we start with a path that looks like: /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Contoso/Databases/{DatabaseName}/ // We want to move to a path that looks like this: /subscriptions/{subscriptionId}/providers/Microsoft.Contoso/Databases/ // To do so, we need to: // 1) Cut out the resource name from the item path let temporaryPath; const index = itemPath.indexOf(resource.collectionName); if (index !== -1) { const truncatedPath = itemPath.slice(0, index + resource.collectionName.length); temporaryPath = truncatedPath; } else { temporaryPath = itemPath; } // 2) If the resource is rg-scoped, trim off the resourceGroups segments const pattern = /\/resourceGroups\/{[^}]+}/; temporaryPath = temporaryPath.replace(pattern, ""); // 3) Trim off all the other `{}` sections (e.g. {ParentResourceName}), minus the {subscriptionId} const pattern1 = /\/{(?!subscriptionId)[^}]+}/; //; return temporaryPath.replace(pattern1, ""); } /** * This function returns fully-resolved details about all ARM resources * registered in the TypeSpec document including operations and their details. * * It should only be called after the full TypeSpec document has been compiled * so that operation route details are certain to be present. */ export function getArmResources(program) { // Have we cached the resolved resource details already? const cachedResources = program.stateMap(ArmStateKeys.armResourcesCached); if (cachedResources.size > 0) { // Return the cached resource details return Array.from(program.stateMap(ArmStateKeys.armResourcesCached).values()); } // We haven't generated the full resource details yet const resources = []; for (const resource of listArmResources(program)) { const fullResource = resolveArmResourceDetails(program, resource); cachedResources.set(resource.typespecType, fullResource); resources.push(fullResource); } return resources; } export const [getResolvedResources, setResolvedResources] = useStateMap(ArmStateKeys.armResolvedResources); export function resolveArmResources(program) { const provider = resolveProviderNamespace(program); if (provider === undefined) return {}; const resolvedResources = getResolvedResources(program, provider); if (resolvedResources?.resources !== undefined && resolvedResources.resources.length > 0) { // Return the cached resource details return resolvedResources; } // We haven't generated the full resource details yet const resources = []; for (const resource of listArmResources(program)) { const operations = resolveArmResourceOperations(program, resource.typespecType); const fullResource = { type: resource.typespecType, kind: getArmResourceKind(resource.typespecType) ?? (operations.length > 0 ? "Tracked" : "Virtual"), providerNamespace: resource.armProviderNamespace, operations: operations, }; resources.push(fullResource); } // Add the unmarked operations const resolved = { resources: resources, unassociatedOperations: getUnassociatedOperations(program).filter((op) => !isArmResourceOperation(program, op.operation)), }; setResolvedResources(program, provider, resolved); return resolved; } function isVariableSegment(segment) { return (segment.startsWith("{") && segment.endsWith("}")) || segment === "default"; } function getResourceInfo(program, operation) { return getResourcePathElements(operation.httpOperation.path, operation.kind); } export function getResourcePathElements(path, kind) { const segments = path.split("/").filter((s) => s.length > 0); const providerIndex = segments.findLastIndex((s) => s === "providers"); if (providerIndex === -1 || providerIndex === segments.length - 1) return undefined; const provider = segments[providerIndex + 1]; const typeSegments = []; const instanceSegments = segments.slice(0, providerIndex + 2); for (let i = providerIndex + 2; i < segments.length; i += 2) { if (isVariableSegment(segments[i])) { return undefined; } if (i + 1 < segments.length && isVariableSegment(segments[i + 1])) { typeSegments.push(segments[i]); instanceSegments.push(segments[i]); instanceSegments.push(segments[i + 1]); } else if (i + 1 === segments.length) { switch (kind) { case "list": typeSegments.push(segments[i]); instanceSegments.push(segments[i]); instanceSegments.push("{name}"); break; default: break; } break; } } if (provider !== undefined && typeSegments.length > 0) { return { resourceType: { provider: provider, types: typeSegments, }, resourceInstancePath: `/${instanceSegments.join("/")}`, }; } return undefined; } function tryAddLifecycleOperation(resourceType, sourceOperation, targetOperation) { const pathSegments = sourceOperation.httpOperation.path .split("/") .filter((s) => s.length > 0); const isInstanceOperation = isVariableSegment(pathSegments[pathSegments.length - 1]); const isResourceCollectionOperation = !isInstanceOperation && pathSegments[pathSegments.length - 1] === resourceType.types[resourceType.types.length - 1]; switch (sourceOperation.httpOperation.verb) { case "get": if (isInstanceOperation) { if (targetOperation.operations.lifecycle.read === undefined) { targetOperation.operations.lifecycle.read = []; } addUniqueOperation(sourceOperation, targetOperation.operations.lifecycle.read); return true; } if (!isResourceCollectionOperation) { addUniqueOperation(sourceOperation, targetOperation.operations.actions || []); return true; } if (targetOperation.operations.lists === undefined) { targetOperation.operations.lists = []; } addUniqueOperation(sourceOperation, targetOperation.operations.lists); return true; case "put": if (!isInstanceOperation) { return false; } if (targetOperation.operations.lifecycle.createOrUpdate === undefined) { targetOperation.operations.lifecycle.createOrUpdate = []; } addUniqueOperation(sourceOperation, targetOperation.operations.lifecycle.createOrUpdate); return true; case "patch": if (!isInstanceOperation) { return false; } if (targetOperation.operations.lifecycle.update === undefined) { targetOperation.operations.lifecycle.update = []; } addUniqueOperation(sourceOperation, targetOperation.operations.lifecycle.update); return true; case "delete": if (!isInstanceOperation) { return false; } if (targetOperation.operations.lifecycle.delete === undefined) { targetOperation.operations.lifecycle.delete = []; } addUniqueOperation(sourceOperation, targetOperation.operations.lifecycle.delete); return true; case "post": case "head": if (isInstanceOperation) { return false; } if (targetOperation.operations.actions === undefined) { targetOperation.operations.actions = []; } addUniqueOperation(sourceOperation, targetOperation.operations.actions); return true; default: return false; } } function addAssociatedOperation(sourceOperation, targetOperation) { if (!targetOperation.associatedOperations) { targetOperation.associatedOperations = []; } addUniqueOperation(sourceOperation, targetOperation.associatedOperations); } export function isResourceOperationMatch(source, target) { if (source.resourceType.provider.toLowerCase() !== target.resourceType.provider.toLowerCase()) return false; if (source.resourceType.types.length !== target.resourceType.types.length) return false; for (let i = 0; i < source.resourceType.types.length; i++) { 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; } return true; } export function getUnassociatedOperations(program) { return getAllOperations(program) .map((op) => getResourceOperation(program, op)) .filter((op) => op !== undefined); } function getResourceOperation(program, operation) { if (operation.kind !== "Operation") return undefined; if (!operation.isFinished) return undefined; if (isTemplateDeclarationOrInstance(operation) && !isTemplateInstance(operation)) return undefined; if (operation.interface === undefined || operation.interface.name === undefined) return undefined; if (isTemplateDeclarationOrInstance(operation.interface) && !isTemplateInstance(operation.interface)) return undefined; const [httpOp, _] = getHttpOperation(program, operation); return { path: httpOp.path, httpOperation: httpOp, name: operation.name, kind: "other", operation: operation, operationGroup: operation.interface.name, }; } function isArmResourceOperation(program, operation) { if (operation.kind !== "Operation") return false; if (operation.isFinished === false) return false; if (isTemplateDeclarationOrInstance(operation) && !isTemplateInstance(operation)) return false; return getArmResourceOperationData(program, operation) !== undefined; } function getAllOperations(program, container) { container = container || resolveProviderNamespace(program); if (!container) { return []; } const operations = []; for (const op of container.operations.values()) { if (op.kind === "Operation" && op.isFinished && (!isTemplateDeclarationOrInstance(op) || isTemplateInstance(op)) && !isArmResourceOperation(program, op)) { operations.push(op); } } if (container.kind === "Namespace") { for (const child of container.namespaces.values()) { operations.push(...getAllOperations(program, child)); } for (const iface of container.interfaces.values()) { operations.push(...getAllOperations(program, iface)); } } return operations; } function addUniqueOperation(operation, operations) { if (!operations.some((op) => op.name.toLowerCase() === operation.name.toLowerCase() && op.operationGroup.toLowerCase() === operation.operationGroup.toLowerCase())) { operations.push(operation); } } export function resolveArmResourceOperations(program, resourceType) { const resolvedOperations = new Set(); const operations = getArmResourceOperationList(program, resourceType); for (const operation of operations) { const armOperation = getResourceOperation(program, operation.operation); if (armOperation === undefined) continue; armOperation.kind = operation.kind; const resourceInfo = getResourceInfo(program, armOperation); if (resourceInfo === undefined) continue; let matched = false; // Check if we already have an operation for this resource for (const resolvedOp of resolvedOperations) { if (isResourceOperationMatch(resourceInfo, resolvedOp)) { matched = true; if (tryAddLifecycleOperation(resourceInfo.resourceType, armOperation, resolvedOp)) { continue; } addAssociatedOperation(armOperation, resolvedOp); continue; } } if (matched) continue; // If we don't have an operation for this resource, create a new one const newOperation = { resourceType: resourceInfo.resourceType, resourceInstancePath: resourceInfo.resourceInstancePath, operations: { lifecycle: { read: undefined, createOrUpdate: undefined, update: undefined, delete: undefined, }, actions: [], lists: [], }, associatedOperations: [], }; if (!tryAddLifecycleOperation(resourceInfo.resourceType, armOperation, newOperation)) { addAssociatedOperation(armOperation, newOperation); } resolvedOperations.add(newOperation); } return [...resolvedOperations.values()].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; const aSegments = a.resourceInstancePath.split("/"); const bSegments = b.resourceInstancePath.split("/"); if (aSegments.length < bSegments.length) return -1; if (aSegments.length > bSegments.length) return 1; if (a.resourceInstancePath.toLowerCase() < b.resourceInstancePath.toLowerCase()) return -1; if (a.resourceInstancePath.toLowerCase() > b.resourceInstancePath.toLowerCase()) return 1; return 0; }); } export { getArmResource } from "./private.decorators.js"; export function getArmResourceInfo(program, resourceType) { const resourceInfo = getArmResource(program, resourceType); if (!resourceInfo && resourceType.namespace !== undefined && !isArmLibraryNamespace(program, resourceType.namespace)) { reportDiagnostic(program, { code: "arm-resource-missing", format: { type: resourceType.name }, target: resourceType, }); } return resourceInfo; } export function getArmResourceKind(resourceType) { if (resourceType.baseModel) { const coreType = resourceType.baseModel; if (coreType.name.startsWith("TrackedResource")) { return "Tracked"; } else if (coreType.name.startsWith("ProxyResource")) { return "Proxy"; } else if (coreType.name.startsWith("ExtensionResource")) { return "Extension"; } } return undefined; } function getResourceOperationOptions(type) { const defaultOptions = { allowStaticRoutes: false, omitTags: false, }; const options = type; if (options === undefined || typeof options !== "object") { return defaultOptions; } return options; } /** * This decorator is used to identify interfaces containing resource operations. * When applied, it marks the interface with the `@autoRoute` decorator so that * all of its contained operations will have their routes generated * automatically. * * It also adds a `@tag` decorator bearing the name of the interface so that all * of the operations will be grouped based on the interface name in generated * clients. */ export const $armResourceOperations = (context, interfaceType, resourceOperationsOptions) => { const { program } = context; const options = getResourceOperationOptions(resourceOperationsOptions); if (!options.allowStaticRoutes) { // All resource interfaces should use @autoRoute context.call($autoRoute, interfaceType); } if (!options.omitTags) { // If no tag is given for the interface, tag it with the interface name if (getTags(program, interfaceType).length === 0) { context.call($tag, interfaceType, interfaceType.name); } } }; /** * This decorator is used to mark a resource type as a "singleton", a type with * only one instance. The standard set of resource operations can be applied to * such a resource type, they will generate the correct routes and parameter * lists. */ export const $singleton = (context, resourceType, keyValue = "default") => { context.program.stateMap(ArmStateKeys.armSingletonResources).set(resourceType, keyValue); }; export function isSingletonResource(program, resourceType) { return program.stateMap(ArmStateKeys.armSingletonResources).has(resourceType); } export function getSingletonResourceKey(program, resourceType) { return program.stateMap(ArmStateKeys.armSingletonResources).get(resourceType); } export var ResourceBaseType; (function (ResourceBaseType) { ResourceBaseType["Tenant"] = "Tenant"; ResourceBaseType["Subscription"] = "Subscription"; ResourceBaseType["Location"] = "Location"; ResourceBaseType["ResourceGroup"] = "ResourceGroup"; ResourceBaseType["Extension"] = "Extension"; ResourceBaseType["BuiltIn"] = "BuiltIn"; ResourceBaseType["BuiltInSubscription"] = "BuiltInSubscription"; ResourceBaseType["BuiltInResourceGroup"] = "BuiltInResourceGroup"; })(ResourceBaseType || (ResourceBaseType = {})); export const $resourceBaseType = (context, entity, baseType) => { let baseTypeString = ""; if (isNeverType(baseType)) return; if (baseType?.kind === "String") baseTypeString = baseType.value; setResourceBaseType(context.program, entity, baseTypeString); }; export const $tenantResource = (context, entity) => { setResourceBaseType(context.program, entity, "Tenant"); }; export const $subscriptionResource = (context, entity) => { setResourceBaseType(context.program, entity, "Subscription"); }; export const $locationResource = (context, entity) => { setResourceBaseType(context.program, entity, "Location"); }; export const $resourceGroupResource = (context, entity) => { setResourceBaseType(context.program, entity, "ResourceGroup"); }; export const $extensionResource = (context, entity) => { setResourceBaseType(context.program, entity, "Extension"); }; export const $armProviderNameValue = (context, entity) => { const armProvider = getServiceNamespace(context.program, entity); if (armProvider === undefined) return; for (const [_, property] of entity.parameters.properties) { const segment = getSegment(context.program, property); if (segment === "providers" && property.type.kind === "String") property.type.value = armProvider; } }; export const $identifiers = (context, entity, properties) => { const { program } = context; const { type } = entity; if (type.kind !== "Model" || !isArrayModelType(program, type) || type.indexer.value.kind !== "Model") { reportDiagnostic(program, { code: "decorator-param-wrong-type", messageId: "armIdentifiersIncorrectEntity", target: entity, }); return; } context.program.stateMap(ArmStateKeys.armIdentifiers).set(entity, properties); }; /** * This function returns identifiers using the @identifiers decorator * * @param program The program to process. * @param entity The array model type to check. * @returns returns list of arm identifiers for the given array model type if any or undefined. */ export function getArmIdentifiers(program, entity) { return program.stateMap(ArmStateKeys.armIdentifiers).get(entity); } /** * This function returns identifiers using the @key decorator. * * @param program The program to process. * @param entity The array model type to check. * @returns returns list of arm identifiers for the given array model type if any or undefined. */ export function getArmKeyIdentifiers(program, entity) { const value = entity.indexer.value; const result = []; if (value.kind === "Model") { for (const property of value.properties.values()) { const pathToKey = getPathToKey(program, property); if (pathToKey !== undefined && !pathToKey.endsWith("/id") && !pathToKey.endsWith("/name")) { result.push(property.name + pathToKey); } else if (getKeyName(program, property) && !["id", "name"].includes(property.name)) { result.push(property.name); } } if (!result.includes("id") && compilerGetProperty(value, "id") !== undefined) { result.push("id"); } } return result.length > 0 ? result : undefined; } function getPathToKey(program, entity, visited = new Set()) { if (entity.type.kind !== "Model") { return undefined; } if (visited.has(entity)) { return undefined; } visited.add(entity); for (const property of entity.type.properties.values()) { if (property.type.kind !== "Model" && getKeyName(program, property)) { return "/" + property.name; } if (property.type.kind === "Model") { const path = getPathToKey(program, property, visited); if (path !== undefined) { return "/" + property.name + path; } } } return undefined; } function getServiceNamespace(program, type) { if (type === undefined) return undefined; switch (type.kind) { case "Operation": return (getServiceNamespace(program, type.namespace) ?? getServiceNamespace(program, type.interface)); case "Interface": return getServiceNamespace(program, type.namespace); case "Namespace": return (getArmProviderNamespace(program, type) ?? (isGlobalNamespace(program, type) ? undefined : getServiceNamespace(program, type.namespace))); default: return undefined; } } export function setResourceBaseType(program, resource, type) { if (program.stateMap(ArmStateKeys.resourceBaseType).has(resource)) { reportDiagnostic(program, { code: "arm-resource-duplicate-base-parameter", target: resource, }); } program.stateMap(ArmStateKeys.resourceBaseType).set(resource, type); } export function getResourceBaseType(program, resource) { const parentTracker = new Set(); let parent = getParentResource(program, resource); while (parent !== undefined) { if (parentTracker.has(parent)) reportDiagnostic(program, { code: "arm-resource-circular-ancestry", target: resource }); parentTracker.add(parent); resource = parent; parent = getParentResource(program, resource); } const keyValue = program .stateMap(ArmStateKeys.resourceBaseType) .get(resource); return resolveResourceBaseType(keyValue); } export function resolveResourceBaseType(type) { let resolvedType = ResourceBaseType.ResourceGroup; if (type !== undefined) { switch (type) { case "Tenant": resolvedType = ResourceBaseType.Tenant; break; case "Subscription": resolvedType = ResourceBaseType.Subscription; break; case "Location": resolvedType = ResourceBaseType.Location; break; case "ResourceGroup": resolvedType = ResourceBaseType.ResourceGroup; break; case "Extension": resolvedType = ResourceBaseType.Extension; break; case "BuiltIn": resolvedType = ResourceBaseType.BuiltIn; break; case "BuiltInSubscription": resolvedType = ResourceBaseType.BuiltInSubscription; break; case "BuiltInResourceGroup": resolvedType = ResourceBaseType.BuiltInResourceGroup; break; } } return resolvedType; } //# sourceMappingURL=resource.js.map