Skip to content

Commit

Permalink
@@customeAzureResource decorator for resources that doesn't use the b…
Browse files Browse the repository at this point in the history
…ase resource types (#1923)

issue: #1474

---------

Co-authored-by: Mark Cowlishaw <[email protected]>
  • Loading branch information
AlitzelMendez and markcowl authored Dec 5, 2024
1 parent 8f21320 commit b29dfc6
Show file tree
Hide file tree
Showing 19 changed files with 311 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-rulesets"
---

Discourage usage of new decorator `@Azure.ResourceManager.Legacy.customAzureResource`
22 changes: 22 additions & 0 deletions packages/typespec-azure-resource-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -283,3 +289,7 @@ export type AzureResourceManagerDecorators = {
armVirtualResource: ArmVirtualResourceDecorator;
resourceBaseType: ResourceBaseTypeDecorator;
};

export type AzureResourceManagerLegacyDecorators = {
customAzureResource: CustomAzureResourceDecorator;
};
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
import "./managed-identity.tsp";
import "./decorator.tsp";
Original file line number Diff line number Diff line change
@@ -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);
2 changes: 2 additions & 0 deletions packages/typespec-azure-resource-manager/src/linter.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -46,6 +47,7 @@ const rules = [
armResourceOperationsRule,
armResourcePathInvalidCharsRule,
armResourceProvisioningStateRule,
armCustomResourceUsageDiscourage,
beyondNestingRule,
coreOperationsRule,
deleteOperationMissingRule,
Expand Down
10 changes: 7 additions & 3 deletions packages/typespec-azure-resource-manager/src/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getArmResourceInfo,
getResourceBaseType,
isArmVirtualResource,
isCustomAzureResource,
ResourceBaseType,
} from "./resource.js";
import { ArmStateKeys } from "./state.js";
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -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 ["", ""];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
getArmResourceKind,
getResourceBaseType,
isArmVirtualResource,
isCustomAzureResource,
resolveResourceBaseType,
} from "./resource.js";
import { ArmStateKeys } from "./state.js";
Expand Down Expand Up @@ -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",
Expand Down
25 changes: 24 additions & 1 deletion packages/typespec-azure-resource-manager/src/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
ArmProviderNameValueDecorator,
ArmResourceOperationsDecorator,
ArmVirtualResourceDecorator,
CustomAzureResourceDecorator,
ExtensionResourceDecorator,
LocationResourceDecorator,
ResourceBaseTypeDecorator,
Expand All @@ -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.
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
});
}
},
};
},
});
1 change: 1 addition & 0 deletions packages/typespec-azure-resource-manager/src/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
9 changes: 8 additions & 1 deletion packages/typespec-azure-resource-manager/src/tsp-index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -15,6 +18,7 @@ import {
$armProviderNameValue,
$armResourceOperations,
$armVirtualResource,
$customAzureResource,
$extensionResource,
$locationResource,
$resourceBaseType,
Expand Down Expand Up @@ -51,6 +55,9 @@ export const $decorators = {
armVirtualResource: $armVirtualResource,
resourceBaseType: $resourceBaseType,
} satisfies AzureResourceManagerDecorators,
"Azure.ResourceManager.Legacy": {
customAzureResource: $customAzureResource,
} satisfies AzureResourceManagerLegacyDecorators,
};

export const $flags = definePackageFlags({
Expand Down
122 changes: 122 additions & 0 deletions packages/typespec-azure-resource-manager/test/resource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<EmployeeProperties> {
...ResourceNameParameter<Employee>;
}
/** 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<Employee>;
}
}
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 () => {
Expand Down Expand Up @@ -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<EmployeeProperties> {
...ResourceNameParameter<Employee>;
}
/** 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<Employee>;
}
}
namespace Microsoft.Person.Contoso {
/** Person parent */
@Azure.ResourceManager.Legacy.customAzureResource
model Person {
/** The parent name */
name: string;
}
}
`);
expectDiagnosticEmpty(diagnostics);
});
Loading

0 comments on commit b29dfc6

Please sign in to comment.