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 `@armCustomResources` decorator to identify ARM resources that do not use the base resource types.
18 changes: 18 additions & 0 deletions packages/typespec-azure-resource-manager/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Available ruleSets:
- [`@armResourceRead`](#@armresourceread)
- [`@armResourceUpdate`](#@armresourceupdate)
- [`@armVirtualResource`](#@armvirtualresource)
- [`@customAzureResource`](#@customazureresource)
- [`@extensionResource`](#@extensionresource)
- [`@locationResource`](#@locationresource)
- [`@resourceBaseType`](#@resourcebasetype)
Expand Down Expand Up @@ -330,6 +331,23 @@ Azure.ResourceManager common types.

None

#### `@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.customAzureResource
```

##### Target

`Model`

##### Parameters

None

#### `@extensionResource`

`@extensionResource` marks an Azure Resource Manager resource model as an Extension resource.
Expand Down
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 @@ -281,5 +287,6 @@ export type AzureResourceManagerDecorators = {
armResourceOperations: ArmResourceOperationsDecorator;
armCommonTypesVersion: ArmCommonTypesVersionDecorator;
armVirtualResource: ArmVirtualResourceDecorator;
customAzureResource: CustomAzureResourceDecorator;
resourceBaseType: ResourceBaseTypeDecorator;
};
6 changes: 6 additions & 0 deletions packages/typespec-azure-resource-manager/lib/decorators.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,12 @@ extern dec armCommonTypesVersion(
*/
extern dec armVirtualResource(target: Model);

/**
* 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);
AlitzelMendez marked this conversation as resolved.
Show resolved Hide resolved

/**
* This decorator sets the base type of the given resource.
*
Expand Down
6 changes: 6 additions & 0 deletions packages/typespec-azure-resource-manager/src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ export const $lib = createTypeSpecLibrary({
default: "Resource types must include a string property called 'name'.",
},
},
"arm-custom-resource-usage-discourage": {
severity: "warning",
messages: {
default: "Avoid using this decorator as it does not provide validation for ARM resources.",
AlitzelMendez marked this conversation as resolved.
Show resolved Hide resolved
},
},
"arm-resource-missing-name-key-decorator": {
severity: "error",
messages: {
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
29 changes: 28 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 @@ -32,9 +33,10 @@ import { reportDiagnostic } from "./lib.js";
import { getArmProviderNamespace, isArmLibraryNamespace } from "./namespace.js";
import { ArmResourceOperations, resolveResourceOperations } from "./operations.js";
import { getArmResource, listArmResources } from "./private.decorators.js";
import { isLegacyTypeSpec } from "./rules/utils.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 +94,19 @@ export const $armVirtualResource: ArmVirtualResourceDecorator = (
}
};

export const $customAzureResource: CustomAzureResourceDecorator = (
context: DecoratorContext,
entity: Model,
) => {
const { program } = context;
if (isTemplateDeclaration(entity)) return;
if (!isLegacyTypeSpec(program, entity)) {
AlitzelMendez marked this conversation as resolved.
Show resolved Hide resolved
reportDiagnostic(program, { code: "arm-custom-resource-usage-discourage", target: entity });
}
program.stateMap(ArmStateKeys.customAzureResource).set(entity, "Custom");
return;
};

function getProperty(
target: Model,
predicate: (property: ModelProperty) => boolean,
Expand All @@ -114,6 +129,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
8 changes: 8 additions & 0 deletions packages/typespec-azure-resource-manager/src/rules/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,14 @@ export function isInternalTypeSpec(
);
}

export function isLegacyTypeSpec(
AlitzelMendez marked this conversation as resolved.
Show resolved Hide resolved
program: Program,
type: Model | Operation | ModelProperty | Interface | Namespace,
): boolean {
const namespace = getNamespaceName(program, type);
return namespace.includes(".Legacy");
}

export function isSourceOperationResourceManagerInternal(
operation: Operation | undefined,
): boolean {
Expand Down
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
2 changes: 2 additions & 0 deletions packages/typespec-azure-resource-manager/src/tsp-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
$armProviderNameValue,
$armResourceOperations,
$armVirtualResource,
$customAzureResource,
$extensionResource,
$locationResource,
$resourceBaseType,
Expand Down Expand Up @@ -48,6 +49,7 @@ export const $decorators = {
armResourceList: $armResourceList,
armResourceOperations: $armResourceOperations,
armCommonTypesVersion: $armCommonTypesVersion,
customAzureResource: $customAzureResource,
armVirtualResource: $armVirtualResource,
resourceBaseType: $resourceBaseType,
} satisfies AzureResourceManagerDecorators,
Expand Down
125 changes: 125 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,57 @@ it("emits required properties for resource with @armResourcePropertiesOptionalit
strictEqual(resources.length, 1);
strictEqual(resources[0].typespecType.properties.get("properties")?.optional, false);
});

it("resource with customResource identifier", async () => {});

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 */
#suppress "@azure-tools/typespec-azure-resource-manager/arm-custom-resource-usage-discourage" "For test"
@customAzureResource
model Person {
/** The parent name */
name: string;
}
}
`);
expectDiagnosticEmpty(diagnostics);
});
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,23 @@ Azure.ResourceManager common types.

None

### `@customAzureResource` {#@Azure.ResourceManager.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.customAzureResource
```

#### Target

`Model`

#### Parameters

None

### `@extensionResource` {#@Azure.ResourceManager.extensionResource}

`@extensionResource` marks an Azure Resource Manager resource model as an Extension resource.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager
- [`@armResourceRead`](./decorators.md#@Azure.ResourceManager.armResourceRead)
- [`@armResourceUpdate`](./decorators.md#@Azure.ResourceManager.armResourceUpdate)
- [`@armVirtualResource`](./decorators.md#@Azure.ResourceManager.armVirtualResource)
- [`@customAzureResource`](./decorators.md#@Azure.ResourceManager.customAzureResource)
- [`@extensionResource`](./decorators.md#@Azure.ResourceManager.extensionResource)
- [`@locationResource`](./decorators.md#@Azure.ResourceManager.locationResource)
- [`@resourceBaseType`](./decorators.md#@Azure.ResourceManager.resourceBaseType)
Expand Down
Loading