diff --git a/common/changes/@microsoft.azure/openapi-validator-rulesets/tenant-resource-fix_2022-11-10-08-15.json b/common/changes/@microsoft.azure/openapi-validator-rulesets/tenant-resource-fix_2022-11-10-08-15.json new file mode 100644 index 000000000..2d69ec453 --- /dev/null +++ b/common/changes/@microsoft.azure/openapi-validator-rulesets/tenant-resource-fix_2022-11-10-08-15.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@microsoft.azure/openapi-validator-rulesets", + "comment": "fix tenant resource '", + "type": "patch" + } + ], + "packageName": "@microsoft.azure/openapi-validator-rulesets" +} \ No newline at end of file diff --git a/packages/rulesets/package.json b/packages/rulesets/package.json index a456bd4a1..57aeba086 100644 --- a/packages/rulesets/package.json +++ b/packages/rulesets/package.json @@ -1,6 +1,6 @@ { "name": "@microsoft.azure/openapi-validator-rulesets", - "version": "1.0.0", + "version": "1.0.1", "description": "Azure OpenAPI Validator", "main": "dist/index.js", "scripts": { diff --git a/packages/rulesets/src/native/functions/arm-resource-validation.ts b/packages/rulesets/src/native/functions/arm-resource-validation.ts index cd2ca5c01..0b6c9619e 100644 --- a/packages/rulesets/src/native/functions/arm-resource-validation.ts +++ b/packages/rulesets/src/native/functions/arm-resource-validation.ts @@ -33,9 +33,8 @@ export function* trackedResourceBeyondsThirdLevel(openapiSection: any, options: // support delete operation for all tracked resource , and all top level proxy resources. export function* allResourcesHaveDelete(openapiSection: any, options: {}, ctx: RuleContext) { const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!) - const allTrackedResources = armHelper.getTrackedResources() - const allTopLevelResource = armHelper.getTopLevelResources() - const allResources = _.uniq(allTrackedResources.concat(allTopLevelResource)) + const allTrackedResources = armHelper.getTrackedResources().filter((re) => !armHelper.isTenantResource(re)) + const allResources = _.uniq(allTrackedResources) for (const re of allResources) { const apiPath = re.operations.find((op) => op.apiPath)?.apiPath if (apiPath) { @@ -170,7 +169,7 @@ export function* resourcesHaveRequiredProperties(openapiSection: any, options: { export function* xmsPageableListByRGAndSubscriptions(openapiSection: any, options: {}, ctx: RuleContext) { const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!) - const trackedResources = armHelper.getTrackedResources() + const trackedResources = armHelper.getTrackedResources().filter((re) => !armHelper.isTenantResource(re)) const collectionApiInfos = armHelper.getCollectionApiInfo() function isListByRgAndSubscription(apiPaths: string[]) { return apiPaths.some((p) => armHelper.isPathByResourceGroup(p)) && apiPaths.some((p) => armHelper.isPathBySubscription(p)) diff --git a/packages/rulesets/src/native/legacyRules/CreateOperationAsyncResponseValidation.ts b/packages/rulesets/src/native/legacyRules/CreateOperationAsyncResponseValidation.ts index 3a666a168..ecc5b3521 100644 --- a/packages/rulesets/src/native/legacyRules/CreateOperationAsyncResponseValidation.ts +++ b/packages/rulesets/src/native/legacyRules/CreateOperationAsyncResponseValidation.ts @@ -20,7 +20,6 @@ rules.push({ location: path.concat(["responses", "202"]), } } - const isAsyncOperation = (node["x-ms-long-running-operation"] && node["x-ms-long-running-operation"] === true) || node["x-ms-long-running-operation-options"] diff --git a/packages/rulesets/src/native/legacyRules/DefaultErrorResponseSchema.ts b/packages/rulesets/src/native/legacyRules/DefaultErrorResponseSchema.ts index 44e7e4293..2af9aacd7 100644 --- a/packages/rulesets/src/native/legacyRules/DefaultErrorResponseSchema.ts +++ b/packages/rulesets/src/native/legacyRules/DefaultErrorResponseSchema.ts @@ -20,9 +20,8 @@ rules.push({ "the default error response schema does not correspond to the schema documented at https://github.com/Azure/azure-resource-manager-rpc/blob/master/v1.0/common-api-details.md#error-response-content." const response: any = node + const paths = path.concat(["default"]) if (response.default && response.default.schema) { - const paths = path.concat(["default"]) - const schema: any = Workspace.jsonPath(paths.concat("schema"), doc) if (schema) { const errorDefinition = Workspace.getProperty({ file: ctx?.specPath!, value: schema }, "error", ctx?.inventory! as SwaggerInventory) @@ -34,6 +33,8 @@ rules.push({ } } } + } + if (response.default) { yield { message: `${msg}`, location: paths } } }, diff --git a/packages/rulesets/src/native/legacyRules/TopLevelResourcesListBySubscription.ts b/packages/rulesets/src/native/legacyRules/TopLevelResourcesListBySubscription.ts index b8b2ebfaf..7d399b1c4 100644 --- a/packages/rulesets/src/native/legacyRules/TopLevelResourcesListBySubscription.ts +++ b/packages/rulesets/src/native/legacyRules/TopLevelResourcesListBySubscription.ts @@ -1,6 +1,7 @@ import { JsonPath, rules, MergeStates, OpenApiTypes } from "@microsoft.azure/openapi-validator-core" import { ArmHelper } from "../utilities/arm-helper" +import { isLikeTenantResourcePath } from "../utilities/rules-helper" export const TopLevelResourcesListBySubscription = "TopLevelResourcesListBySubscription" rules.push({ @@ -14,15 +15,16 @@ rules.push({ *run(doc, node, path, ctx) { const msg = 'The top-level resource "{0}" does not have list by subscription operation, please add it.' const utils = new ArmHelper(doc, ctx?.specPath!, ctx?.inventory!) - const topLevelResources = utils.getTopLevelResourceNames() + const topLevelResources = utils.getTopLevelResources().filter((re) => !re.operations.some((op) => isLikeTenantResourcePath(op.apiPath))) const allCollectionApis = utils.getCollectionApiInfo() for (const resource of topLevelResources) { const hasMatched = allCollectionApis.some( - (collection) => resource === collection.childModelName && collection.collectionGetPath.some((p) => utils.isPathBySubscription(p)) + (collection) => + resource.modelName === collection.childModelName && collection.collectionGetPath.some((p) => utils.isPathBySubscription(p)) ) if (!hasMatched) { yield { - message: msg.replace("{0}", resource), + message: msg.replace("{0}", resource.modelName), location: ["$", "definitions", resource] as JsonPath, } } diff --git a/packages/rulesets/src/native/rulesets/arm.ts b/packages/rulesets/src/native/rulesets/arm.ts index 319e9af6e..a20b52420 100644 --- a/packages/rulesets/src/native/rulesets/arm.ts +++ b/packages/rulesets/src/native/rulesets/arm.ts @@ -33,7 +33,7 @@ export const armRuleset: IRuleSet = { }, }, // https://github.com/Azure/azure-openapi-validator/issues/329 - AllResourcesMustHaveDelete: { + TrackedResourcesMustHaveDelete: { category: "ARMViolation", openapiType: OpenApiTypes.arm, severity: "error", diff --git a/packages/rulesets/src/native/tests/individual-azure-tests.ts b/packages/rulesets/src/native/tests/individual-azure-tests.ts index 94d8a82e4..0d47914d4 100644 --- a/packages/rulesets/src/native/tests/individual-azure-tests.ts +++ b/packages/rulesets/src/native/tests/individual-azure-tests.ts @@ -410,7 +410,7 @@ describe("IndividualAzureTests", () => { test("no delete in for tracked resource", async () => { const fileNames = ["armResource/trackedResourceNoDelete.json", "armResource/trackedResourceCommon.json"] - const ruleName = "AllResourcesMustHaveDelete" + const ruleName = "TrackedResourcesMustHaveDelete" const messages: LintResultMessage[] = await collectTestMessagesFromValidator(fileNames, OpenApiTypes.arm, ruleName) assertValidationRuleCount(messages, ruleName, 1) }) diff --git a/packages/rulesets/src/native/utilities/arm-helper.ts b/packages/rulesets/src/native/utilities/arm-helper.ts index 2f05ff4b7..cb5c2e250 100644 --- a/packages/rulesets/src/native/utilities/arm-helper.ts +++ b/packages/rulesets/src/native/utilities/arm-helper.ts @@ -5,6 +5,7 @@ import { ISwaggerInventory, parseJsonRef } from "@microsoft.azure/openapi-validator-core" import _ from "lodash" import { nodes } from "./jsonpath" +import { isLikeTenantResourcePath } from "./rules-helper" import { SwaggerHelper } from "./swagger-helper" import { SwaggerWalker } from "./swagger-walker" import { Workspace } from "./swagger-workspace" @@ -299,6 +300,10 @@ export class ArmHelper { return allTrackedResources } + public isTenantResource(re: ResourceInfo) { + return re.operations.some((op) => isLikeTenantResourcePath(op.apiPath)) + } + public getAllResourceNames() { const fullResources = this.getAllResources() const resources = new Set() diff --git a/packages/rulesets/src/native/utilities/rules-helper.ts b/packages/rulesets/src/native/utilities/rules-helper.ts index 754e32571..e4c04563f 100644 --- a/packages/rulesets/src/native/utilities/rules-helper.ts +++ b/packages/rulesets/src/native/utilities/rules-helper.ts @@ -56,7 +56,7 @@ export function getAllResourceProvidersFromPath(path: string): string[] { return Array.from(matchAll(path, resourceProviderRegex), (m: any) => m[1]) } -export function getProviderNamespace(apiPath:string) { +export function getProviderNamespace(apiPath: string) { const matches = getAllResourceProvidersFromPath(apiPath) if (matches.length) { return matches.pop() @@ -64,19 +64,18 @@ export function getProviderNamespace(apiPath:string) { return undefined } -export function getProviderNamespaceFromPath(filePath:string) { +export function getProviderNamespaceFromPath(filePath: string) { if (!filePath) { return undefined } const resourceProviderRegex = new RegExp(/\/(Microsoft\.\w+)\//i, "g") - const match = Array.from(matchAll(filePath.replace(/\\/g,"/"),resourceProviderRegex), (m: any) => m[1]) + const match = Array.from(matchAll(filePath.replace(/\\/g, "/"), resourceProviderRegex), (m: any) => m[1]) if (match) { return match[0] } return undefined } - export function getAllWordsFromPath(path: string): string[] { const wordRegex = new RegExp(/([\w.]+)/, "g") return Array.from(matchAll(path, wordRegex), (m: any) => m[1]) @@ -138,14 +137,18 @@ export function stringify(path: string[]) { return JSONPath.toPathString(pathWithRoot) } -export function getResourceProvider(inventory:ISwaggerInventory) { +export function getResourceProvider(inventory: ISwaggerInventory) { const walker = new SwaggerWalker(inventory) - let result: string[] = [] - walker.warkAll(["$.paths.*"], (path: string[]) => { - const apiPath = path[2] as string - if (result.length === 0) { - result = [...getAllResourceProvidersFromPath(apiPath)] - } - }) - return result.length ? result.pop() || "" : "" -} \ No newline at end of file + let result: string[] = [] + walker.warkAll(["$.paths.*"], (path: string[]) => { + const apiPath = path[2] as string + if (result.length === 0) { + result = [...getAllResourceProvidersFromPath(apiPath)] + } + }) + return result.length ? result.pop() || "" : "" +} + +export function isLikeTenantResourcePath(path: string) { + return path.toLowerCase().startsWith("/providers/") +} diff --git a/rush.json b/rush.json index 117b4feb5..b2a152c41 100644 --- a/rush.json +++ b/rush.json @@ -18,15 +18,18 @@ "projects": [ { "packageName": "@microsoft.azure/openapi-validator-core", - "projectFolder": "packages/azure-openapi-validator/core" + "projectFolder": "packages/azure-openapi-validator/core", + "shouldPublish": true }, { "packageName": "@microsoft.azure/openapi-validator", - "projectFolder": "packages/azure-openapi-validator/autorest" + "projectFolder": "packages/azure-openapi-validator/autorest", + "shouldPublish": true }, { "packageName": "@microsoft.azure/openapi-validator-rulesets", - "projectFolder": "packages/rulesets" + "projectFolder": "packages/rulesets", + "shouldPublish": true }, { "packageName": "openapi-validator-regression",