Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@@customeAzureResource decorator for resources that doesn't use the base resource types #1923

Merged
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
Loading