diff --git a/.chronus/changes/new-decorator-custom-azure-resource-2024-10-27-15-43-13.md b/.chronus/changes/new-decorator-custom-azure-resource-2024-10-27-15-43-13.md new file mode 100644 index 0000000000..7c9994d549 --- /dev/null +++ b/.chronus/changes/new-decorator-custom-azure-resource-2024-10-27-15-43-13.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-resource-manager" +--- + +Add the `@Azure.ResourceManager.Legacy.customAzureResource` decorator to identify ARM resources that do not use the base resource types. diff --git a/.chronus/changes/new-decorator-custom-azure-resource-2024-11-5-11-0-40.md b/.chronus/changes/new-decorator-custom-azure-resource-2024-11-5-11-0-40.md new file mode 100644 index 0000000000..cbb09e4ddb --- /dev/null +++ b/.chronus/changes/new-decorator-custom-azure-resource-2024-11-5-11-0-40.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-azure-rulesets" +--- + +Discourage usage of new decorator `@Azure.ResourceManager.Legacy.customAzureResource` diff --git a/packages/typespec-azure-resource-manager/README.md b/packages/typespec-azure-resource-manager/README.md index ede6bf0b85..83f2165fc5 100644 --- a/packages/typespec-azure-resource-manager/README.md +++ b/packages/typespec-azure-resource-manager/README.md @@ -42,6 +42,7 @@ Available ruleSets: | `@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response` | [RPC 008]: PUT, GET, PATCH & LIST must return the same resource schema. | | `@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars` | Arm resource name must contain only alphanumeric characters. | | `@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state` | Check for properly configured provisioningState property. | +| `@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage` | Verify the usage of @customAzureResource decorator. | | `@azure-tools/typespec-azure-resource-manager/beyond-nesting-levels` | Tracked Resources must use 3 or fewer levels of nesting. | | `@azure-tools/typespec-azure-resource-manager/arm-resource-operation` | Validate ARM Resource operations. | | `@azure-tools/typespec-azure-resource-manager/no-resource-delete-operation` | Check for resources that must have a delete operation. | @@ -496,3 +497,24 @@ This allows sharing Azure Resource Manager resource types across specifications | Name | Type | Description | | ---------- | ------------- | ------------------------------------------------------------------------ | | namespaces | `Namespace[]` | The namespaces of Azure Resource Manager libraries used in this provider | + +### Azure.ResourceManager.Legacy + +- [`@customAzureResource`](#@customazureresource) + +#### `@customAzureResource` + +This decorator is used on resources that do not satisfy the definition of a resource +but need to be identified as such. + +```typespec +@Azure.ResourceManager.Legacy.customAzureResource +``` + +##### Target + +`Model` + +##### Parameters + +None diff --git a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts index 5583d8206e..e956b75325 100644 --- a/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts +++ b/packages/typespec-azure-resource-manager/generated-defs/Azure.ResourceManager.ts @@ -249,6 +249,12 @@ export type ArmCommonTypesVersionDecorator = ( */ export type ArmVirtualResourceDecorator = (context: DecoratorContext, target: Model) => void; +/** + * This decorator is used on resources that do not satisfy the definition of a resource + * but need to be identified as such. + */ +export type CustomAzureResourceDecorator = (context: DecoratorContext, target: Model) => void; + /** * This decorator sets the base type of the given resource. * @@ -283,3 +289,7 @@ export type AzureResourceManagerDecorators = { armVirtualResource: ArmVirtualResourceDecorator; resourceBaseType: ResourceBaseTypeDecorator; }; + +export type AzureResourceManagerLegacyDecorators = { + customAzureResource: CustomAzureResourceDecorator; +}; diff --git a/packages/typespec-azure-resource-manager/lib/Legacy/arm.legacy.tsp b/packages/typespec-azure-resource-manager/lib/Legacy/arm.legacy.tsp index 65fe4b9ae1..0d7d1f84ec 100644 --- a/packages/typespec-azure-resource-manager/lib/Legacy/arm.legacy.tsp +++ b/packages/typespec-azure-resource-manager/lib/Legacy/arm.legacy.tsp @@ -1 +1,2 @@ import "./managed-identity.tsp"; +import "./decorator.tsp"; diff --git a/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp b/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp new file mode 100644 index 0000000000..d00ce1ed28 --- /dev/null +++ b/packages/typespec-azure-resource-manager/lib/Legacy/decorator.tsp @@ -0,0 +1,9 @@ +using TypeSpec.Reflection; + +namespace Azure.ResourceManager.Legacy; + +/** + * This decorator is used on resources that do not satisfy the definition of a resource + * but need to be identified as such. + */ +extern dec customAzureResource(target: Model); diff --git a/packages/typespec-azure-resource-manager/src/linter.ts b/packages/typespec-azure-resource-manager/src/linter.ts index 09be44f29e..563f47ab9a 100644 --- a/packages/typespec-azure-resource-manager/src/linter.ts +++ b/packages/typespec-azure-resource-manager/src/linter.ts @@ -1,5 +1,6 @@ import { defineLinter } from "@typespec/compiler"; import { armCommonTypesVersionRule } from "./rules/arm-common-types-version.js"; +import { armCustomResourceUsageDiscourage } from "./rules/arm-custom-resource-usage-discourage.js"; import { armDeleteResponseCodesRule } from "./rules/arm-delete-response-codes.js"; import { armNoRecordRule } from "./rules/arm-no-record.js"; import { armPostResponseCodesRule } from "./rules/arm-post-response-codes.js"; @@ -46,6 +47,7 @@ const rules = [ armResourceOperationsRule, armResourcePathInvalidCharsRule, armResourceProvisioningStateRule, + armCustomResourceUsageDiscourage, beyondNestingRule, coreOperationsRule, deleteOperationMissingRule, diff --git a/packages/typespec-azure-resource-manager/src/operations.ts b/packages/typespec-azure-resource-manager/src/operations.ts index 5be5806562..60236a83e4 100644 --- a/packages/typespec-azure-resource-manager/src/operations.ts +++ b/packages/typespec-azure-resource-manager/src/operations.ts @@ -33,6 +33,7 @@ import { getArmResourceInfo, getResourceBaseType, isArmVirtualResource, + isCustomAzureResource, ResourceBaseType, } from "./resource.js"; import { ArmStateKeys } from "./state.js"; @@ -234,8 +235,11 @@ export function armRenameListByOperationInternal( [parentTypeName, parentFriendlyTypeName] = getArmParentName(context.program, resourceType); } const parentType = getParentResource(program, resourceType); - - if (parentType && !isArmVirtualResource(program, parentType)) { + if ( + parentType && + !isArmVirtualResource(program, parentType) && + !isCustomAzureResource(program, parentType) + ) { const parentResourceInfo = getArmResourceInfo(program, parentType); if ( !parentResourceInfo && @@ -278,7 +282,7 @@ export function armRenameListByOperationInternal( function getArmParentName(program: Program, resource: Model): string[] { const parent = getParentResource(program, resource); - if (parent && isArmVirtualResource(program, parent)) { + if (parent && (isArmVirtualResource(program, parent) || isCustomAzureResource(program, parent))) { const parentName = getFriendlyName(program, parent) ?? parent.name; if (parentName === undefined || parentName.length < 2) { return ["", ""]; diff --git a/packages/typespec-azure-resource-manager/src/private.decorators.ts b/packages/typespec-azure-resource-manager/src/private.decorators.ts index 128438e753..46b9896b6f 100644 --- a/packages/typespec-azure-resource-manager/src/private.decorators.ts +++ b/packages/typespec-azure-resource-manager/src/private.decorators.ts @@ -40,6 +40,7 @@ import { getArmResourceKind, getResourceBaseType, isArmVirtualResource, + isCustomAzureResource, resolveResourceBaseType, } from "./resource.js"; import { ArmStateKeys } from "./state.js"; @@ -325,6 +326,7 @@ const $armResourceInternal: ArmResourceInternalDecorator = ( let kind = getArmResourceKind(resourceType); if (isArmVirtualResource(program, resourceType)) kind = "Virtual"; + if (isCustomAzureResource(program, resourceType)) kind = "Custom"; if (!kind) { reportDiagnostic(program, { code: "arm-resource-invalid-base-type", diff --git a/packages/typespec-azure-resource-manager/src/resource.ts b/packages/typespec-azure-resource-manager/src/resource.ts index 232965e9e5..ecfc16d292 100644 --- a/packages/typespec-azure-resource-manager/src/resource.ts +++ b/packages/typespec-azure-resource-manager/src/resource.ts @@ -20,6 +20,7 @@ import { ArmProviderNameValueDecorator, ArmResourceOperationsDecorator, ArmVirtualResourceDecorator, + CustomAzureResourceDecorator, ExtensionResourceDecorator, LocationResourceDecorator, ResourceBaseTypeDecorator, @@ -34,7 +35,7 @@ import { ArmResourceOperations, resolveResourceOperations } from "./operations.j import { getArmResource, listArmResources } from "./private.decorators.js"; import { ArmStateKeys } from "./state.js"; -export type ArmResourceKind = "Tracked" | "Proxy" | "Extension" | "Virtual"; +export type ArmResourceKind = "Tracked" | "Proxy" | "Extension" | "Virtual" | "Custom"; /** * Interface for ARM resource detail base. @@ -92,6 +93,16 @@ export const $armVirtualResource: ArmVirtualResourceDecorator = ( } }; +export const $customAzureResource: CustomAzureResourceDecorator = ( + context: DecoratorContext, + entity: Model, +) => { + const { program } = context; + if (isTemplateDeclaration(entity)) return; + + program.stateMap(ArmStateKeys.customAzureResource).set(entity, "Custom"); +}; + function getProperty( target: Model, predicate: (property: ModelProperty) => boolean, @@ -114,6 +125,18 @@ export function isArmVirtualResource(program: Program, target: Model): boolean { return false; } +/** + * 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: Program, target: Model): boolean { + if (program.stateMap(ArmStateKeys.customAzureResource).has(target)) return true; + if (target.baseModel) return isCustomAzureResource(program, target.baseModel); + return false; +} + function resolveArmResourceDetails( program: Program, resource: ArmResourceDetailsBase, diff --git a/packages/typespec-azure-resource-manager/src/rules/arm-custom-resource-usage-discourage.ts b/packages/typespec-azure-resource-manager/src/rules/arm-custom-resource-usage-discourage.ts new file mode 100644 index 0000000000..f4e60dc323 --- /dev/null +++ b/packages/typespec-azure-resource-manager/src/rules/arm-custom-resource-usage-discourage.ts @@ -0,0 +1,23 @@ +import { createRule, Model } from "@typespec/compiler"; +import { isCustomAzureResource } from "../resource.js"; + +export const armCustomResourceUsageDiscourage = createRule({ + name: "arm-custom-resource-usage-discourage", + severity: "warning", + description: "Verify the usage of @customAzureResource decorator.", + messages: { + default: `Avoid using this decorator except when converting old APIs to TypeSpec, as it does not provide validation for ARM resources.`, + }, + create(context) { + return { + model: (model: Model) => { + if (isCustomAzureResource(context.program, model)) { + context.reportDiagnostic({ + code: "arm-custom-resource-usage-discourage", + target: model, + }); + } + }, + }; + }, +}); diff --git a/packages/typespec-azure-resource-manager/src/state.ts b/packages/typespec-azure-resource-manager/src/state.ts index e6ba7365cd..7323a184d3 100644 --- a/packages/typespec-azure-resource-manager/src/state.ts +++ b/packages/typespec-azure-resource-manager/src/state.ts @@ -20,6 +20,7 @@ export const ArmStateKeys = { armSingletonResources: azureResourceManagerCreateStateSymbol("armSingletonResources"), resourceBaseType: azureResourceManagerCreateStateSymbol("resourceBaseTypeKey"), armBuiltInResource: azureResourceManagerCreateStateSymbol("armExternalResource"), + customAzureResource: azureResourceManagerCreateStateSymbol("azureCustomResource"), // private.decorator.ts azureResourceBase: azureResourceManagerCreateStateSymbol("azureResourceBase"), diff --git a/packages/typespec-azure-resource-manager/src/tsp-index.ts b/packages/typespec-azure-resource-manager/src/tsp-index.ts index 1766ca62f3..2d0a27ea1b 100644 --- a/packages/typespec-azure-resource-manager/src/tsp-index.ts +++ b/packages/typespec-azure-resource-manager/src/tsp-index.ts @@ -1,5 +1,8 @@ import { definePackageFlags } from "@typespec/compiler"; -import { AzureResourceManagerDecorators } from "../generated-defs/Azure.ResourceManager.js"; +import { + AzureResourceManagerDecorators, + AzureResourceManagerLegacyDecorators, +} from "../generated-defs/Azure.ResourceManager.js"; import { $armCommonTypesVersion } from "./common-types.js"; import { $armLibraryNamespace, $armProviderNamespace, $useLibraryNamespace } from "./namespace.js"; import { @@ -15,6 +18,7 @@ import { $armProviderNameValue, $armResourceOperations, $armVirtualResource, + $customAzureResource, $extensionResource, $locationResource, $resourceBaseType, @@ -51,6 +55,9 @@ export const $decorators = { armVirtualResource: $armVirtualResource, resourceBaseType: $resourceBaseType, } satisfies AzureResourceManagerDecorators, + "Azure.ResourceManager.Legacy": { + customAzureResource: $customAzureResource, + } satisfies AzureResourceManagerLegacyDecorators, }; export const $flags = definePackageFlags({ diff --git a/packages/typespec-azure-resource-manager/test/resource.test.ts b/packages/typespec-azure-resource-manager/test/resource.test.ts index b7f6884758..2577afb9d8 100644 --- a/packages/typespec-azure-resource-manager/test/resource.test.ts +++ b/packages/typespec-azure-resource-manager/test/resource.test.ts @@ -716,6 +716,77 @@ describe("typespec-azure-resource-manager: ARM resource model", () => { strictEqual(nameProperty?.type.kind, "Scalar"); strictEqual(nameProperty?.type.name, "WidgetNameType"); }); + + it("emits diagnostics for non ARM resources", async () => { + const { diagnostics } = await checkFor(` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso { + @parentResource(Microsoft.Person.Contoso.Person) + model Employee is TrackedResource { + ...ResourceNameParameter; + } + + /** Employee properties */ + model EmployeeProperties { + /** The status of the last operation. */ + @visibility("read") + provisioningState?: ProvisioningState; + } + + /** The provisioning state of a resource. */ + union ProvisioningState { + string, + + /** Resource has been created. */ + Succeeded: "Succeeded", + + /** Resource creation failed. */ + Failed: "Failed", + + /** Resource creation was canceled. */ + Canceled: "Canceled", + } + + interface Operations extends Azure.ResourceManager.Operations {} + + @armResourceOperations + interface Employees { + listByResourceGroup is ArmResourceListByParent; + } + } + + namespace Microsoft.Person.Contoso { + /** Person parent */ + model Person { + /** The parent name */ + @path + @visibility("read") + @segment("parents") + @key + name: string; + } + } +`); + expectDiagnostics(diagnostics, [ + { + code: "@azure-tools/typespec-azure-resource-manager/arm-resource-missing", + message: "No @armResource registration found for type Person", + }, + { + code: "@azure-tools/typespec-azure-resource-manager/parent-type", + message: "Parent type Person of Employee is not registered as an ARM resource type.", + }, + { + code: "@azure-tools/typespec-azure-resource-manager/arm-resource-missing", + message: "No @armResource registration found for type Person", + }, + { + code: "@azure-tools/typespec-azure-resource-manager/parent-type", + message: "Parent type Person of Employee is not registered as an ARM resource type.", + }, + ]); + }); }); it("emits default optional properties for resource", async () => { @@ -761,3 +832,54 @@ it("emits required properties for resource with @armResourcePropertiesOptionalit strictEqual(resources.length, 1); strictEqual(resources[0].typespecType.properties.get("properties")?.optional, false); }); + +it("recognizes resource with customResource identifier", async () => { + const { diagnostics } = await checkFor(` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso { + @parentResource(Microsoft.Person.Contoso.Person) + model Employee is TrackedResource { + ...ResourceNameParameter; + } + + /** Employee properties */ + model EmployeeProperties { + /** The status of the last operation. */ + @visibility("read") + provisioningState?: ProvisioningState; + } + + /** The provisioning state of a resource. */ + union ProvisioningState { + string, + + /** Resource has been created. */ + Succeeded: "Succeeded", + + /** Resource creation failed. */ + Failed: "Failed", + + /** Resource creation was canceled. */ + Canceled: "Canceled", + } + + interface Operations extends Azure.ResourceManager.Operations {} + + @armResourceOperations + interface Employees { + listByResourceGroup is ArmResourceListByParent; + } + } + + namespace Microsoft.Person.Contoso { + /** Person parent */ + @Azure.ResourceManager.Legacy.customAzureResource + model Person { + /** The parent name */ + name: string; + } + } +`); + expectDiagnosticEmpty(diagnostics); +}); diff --git a/packages/typespec-azure-resource-manager/test/rules/arm-custom-resource-usage-discourage.test.ts b/packages/typespec-azure-resource-manager/test/rules/arm-custom-resource-usage-discourage.test.ts new file mode 100644 index 0000000000..d47239d807 --- /dev/null +++ b/packages/typespec-azure-resource-manager/test/rules/arm-custom-resource-usage-discourage.test.ts @@ -0,0 +1,41 @@ +import { + BasicTestRunner, + LinterRuleTester, + createLinterRuleTester, +} from "@typespec/compiler/testing"; +import { beforeEach, it } from "vitest"; +import { armCustomResourceUsageDiscourage } from "../../src/rules/arm-custom-resource-usage-discourage.js"; +import { createAzureResourceManagerTestRunner } from "../test-host.js"; + +let runner: BasicTestRunner; +let tester: LinterRuleTester; + +beforeEach(async () => { + runner = await createAzureResourceManagerTestRunner(); + tester = createLinterRuleTester( + runner, + armCustomResourceUsageDiscourage, + "@azure-tools/typespec-azure-resource-manager", + ); +}); + +it("emits diagnostic when using @Azure.ResourceManager.Legacy.customAzureResource decorator", async () => { + await tester + .expect( + ` + @armProviderNamespace + @useDependency(Azure.ResourceManager.Versions.v1_0_Preview_1) + namespace Microsoft.Contoso; + + @Azure.ResourceManager.Legacy.customAzureResource + model Person { + name: string; + } + `, + ) + .toEmitDiagnostics({ + code: "@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage", + message: + "Avoid using this decorator except when converting old APIs to TypeSpec, as it does not provide validation for ARM resources.", + }); +}); diff --git a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts index d3397a884e..a6da8a8bea 100644 --- a/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts +++ b/packages/typespec-azure-rulesets/src/rulesets/resource-manager.ts @@ -65,6 +65,7 @@ export default { "@azure-tools/typespec-azure-resource-manager/arm-resource-invalid-version-format": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-key-invalid-chars": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-name-pattern": true, + "@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars": true, "@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state": true, diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md index eabf425fd7..9b2cc18146 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/decorators.md @@ -416,3 +416,22 @@ This allows sharing Azure Resource Manager resource types across specifications | Name | Type | Description | | ---------- | ------------- | ------------------------------------------------------------------------ | | namespaces | `Namespace[]` | The namespaces of Azure Resource Manager libraries used in this provider | + +## Azure.ResourceManager.Legacy + +### `@customAzureResource` {#@Azure.ResourceManager.Legacy.customAzureResource} + +This decorator is used on resources that do not satisfy the definition of a resource +but need to be identified as such. + +```typespec +@Azure.ResourceManager.Legacy.customAzureResource +``` + +#### Target + +`Model` + +#### Parameters + +None diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx index fa54489830..88fbc6bd48 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx @@ -266,6 +266,10 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager ## Azure.ResourceManager.Legacy +### Decorators + +- [`@customAzureResource`](./decorators.md#@Azure.ResourceManager.Legacy.customAzureResource) + ### Models - [`ManagedServiceIdentityV4`](./data-types.md#Azure.ResourceManager.Legacy.ManagedServiceIdentityV4) diff --git a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md index b5ea735027..2c0f2012ef 100644 --- a/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md +++ b/website/src/content/docs/docs/libraries/azure-resource-manager/reference/linter.md @@ -36,6 +36,7 @@ Available ruleSets: | `@azure-tools/typespec-azure-resource-manager/arm-resource-operation-response` | [RPC 008]: PUT, GET, PATCH & LIST must return the same resource schema. | | `@azure-tools/typespec-azure-resource-manager/arm-resource-path-segment-invalid-chars` | Arm resource name must contain only alphanumeric characters. | | `@azure-tools/typespec-azure-resource-manager/arm-resource-provisioning-state` | Check for properly configured provisioningState property. | +| `@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage` | Verify the usage of @customAzureResource decorator. | | `@azure-tools/typespec-azure-resource-manager/beyond-nesting-levels` | Tracked Resources must use 3 or fewer levels of nesting. | | `@azure-tools/typespec-azure-resource-manager/arm-resource-operation` | Validate ARM Resource operations. | | `@azure-tools/typespec-azure-resource-manager/no-resource-delete-operation` | Check for resources that must have a delete operation. |