Skip to content

Commit 2bff530

Browse files
authored
Added new rule LroAzureAsyncOperationHeader (#749)
* Added new rule LroAzureAsyncOperationHeader * I've addressed the comments and tweaked the logic to fit the rule for all LRO operations. * build failure * addressed comments * Reverted to initial logic to address all LRO operations to have Azure-AsyncOperation headers * added new function to check existence of both headers and Azure-AsyncOperation case sensitive * somehow few changes not reflected pushing them again, and also addressed comments * trying to push changes again * removed extra . * added one more bad example
1 parent 6ef7723 commit 2bff530

File tree

6 files changed

+286
-0
lines changed

6 files changed

+286
-0
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# LroAzureAsyncOperationHeader
2+
3+
## Category
4+
5+
ARM Error
6+
7+
## Applies to
8+
9+
ARM OpenAPI(swagger) specs
10+
11+
## Related ARM Guideline Code
12+
13+
- RPC-Async-V1-06
14+
15+
## Output Message
16+
17+
All long-running operations must include an `Azure-AsyncOperation` response header.
18+
19+
## Description
20+
21+
ARM relies on the async operation header to poll for the status of the long running operation. Based on this and the
22+
final state of the operation, downstream services like ARN and ARG are notified of the current state of the operation
23+
and the status of the resource. If you are a brownfield service that does not implement this header, you may add a
24+
suppression using the following TSG indicating the same.
25+
TSG link - https://github.com/Azure/autorest/blob/main/docs/generate/suppress-warnings.md.
26+
In the description for the suppression, please provide a rough timeline by which the header will be supported by your
27+
service. More details about this header can be found in the ARM Resource Provider Contract documentation here - https://github.com/cloud-and-ai-microsoft/resource-provider-contract/blob/master/v1.0/async-api-reference.md#azure-asyncoperation-resource-format
28+
29+
## CreatedAt
30+
31+
Oct 11, 2024
32+
33+
## How to fix the violation
34+
35+
Adding the Azure-AsyncOperation header to the response.
36+
37+
## Good Example
38+
39+
```json
40+
"/api/configServers": {
41+
"put": {
42+
"operationId": "ConfigServers_Update",
43+
"responses": {
44+
"202": {
45+
"description": "Accepted",
46+
"headers": {
47+
"Azure-AsyncOperation": {
48+
"type": "string",
49+
},
50+
},
51+
},
52+
},
53+
},
54+
},
55+
```
56+
57+
## Bad Example 1
58+
59+
```json
60+
"/api/configServers": {
61+
"put": {
62+
"operationId": "ConfigServers_Update",
63+
"responses": {
64+
"202": {
65+
"description": "Success",
66+
"headers": {
67+
//No Azure-AsyncOperation header
68+
},
69+
},
70+
},
71+
},
72+
},
73+
```
74+
75+
## Bad Example 2
76+
77+
```json
78+
"/api/configServers": {
79+
"put": {
80+
"operationId": "ConfigServers_Update",
81+
"responses": {
82+
"202": {
83+
"description": "Success",
84+
//No headers
85+
},
86+
},
87+
},
88+
},

docs/rules.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -495,6 +495,18 @@ For Data plane spec, the allowed response status codes for a long DELETE operati
495495

496496
Please refer to [long-running-response-status-code-data-plane.md](./long-running-response-status-code-data-plane.md) for details.
497497

498+
### LroAzureAsyncOperationHeader
499+
500+
ARM relies on the async operation header to poll for the status of the long running operation. Based on this and the
501+
final state of the operation, downstream services like ARN and ARG are notified of the current state of the operation
502+
and the status of the resource. If you are a brownfield service that does not implement this header, you may add a
503+
suppression using the following TSG indicating the same.
504+
TSG link - https://github.com/Azure/autorest/blob/main/docs/generate/suppress-warnings.md.
505+
In the description for the suppression, please provide a rough timeline by which the header will be supported by your
506+
service. More details about this header can be found in the ARM Resource Provider Contract documentation here - https://github.com/cloud-and-ai-microsoft/resource-provider-contract/blob/master/v1.0/async-api-reference.md#azure-asyncoperation-resource-format
507+
508+
Please refer to [lro-azure-async-operation-header.md](./lro-azure-async-operation-header.md) for details.
509+
498510
### LroErrorContent
499511

500512
Error response content of long running operations must follow the error schema provided in the common types v2 and above.

packages/rulesets/generated/spectral/az-arm.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,18 @@ const locationMustHaveXmsMutability = (scheme, _opts, paths) => {
17431743
}];
17441744
};
17451745

1746+
const lroAzureAsyncOperationHeader = (headers, _opts, ctx) => {
1747+
if (!Object.keys(headers).includes("headers") || !Object.keys(headers.headers).includes("Azure-AsyncOperation")) {
1748+
return [
1749+
{
1750+
message: "All long-running operations must include an `Azure-AsyncOperation` response header.",
1751+
path: ctx.path.concat("headers"),
1752+
},
1753+
];
1754+
}
1755+
return [];
1756+
};
1757+
17461758
const validateOriginalUri = (lroOptions, opts, ctx) => {
17471759
if (!lroOptions || typeof lroOptions !== "object") {
17481760
return [];
@@ -3311,6 +3323,17 @@ const ruleset = {
33113323
function: provisioningState,
33123324
},
33133325
},
3326+
LroAzureAsyncOperationHeader: {
3327+
rpcGuidelineCode: "RPC-Async-V1-06",
3328+
description: "All long-running operations must include an `Azure-AsyncOperation` response header.",
3329+
message: "{{description}}",
3330+
severity: "error",
3331+
formats: [oas2],
3332+
given: ["$[paths,'x-ms-paths'].*.*[?(@property === 'x-ms-long-running-operation' && @ === true)]^.responses.*"],
3333+
then: {
3334+
function: lroAzureAsyncOperationHeader,
3335+
},
3336+
},
33143337
LroLocationHeader: {
33153338
rpcGuidelineCode: "RPC-Async-V1-07",
33163339
description: "Location header must be supported for all async operations that return 202.",

packages/rulesets/src/spectral/az-arm.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import hasheader from "./functions/has-header"
1515
import httpsSupportedScheme from "./functions/https-supported-scheme"
1616
import { latestVersionOfCommonTypesMustBeUsed } from "./functions/latest-version-of-common-types-must-be-used"
1717
import locationMustHaveXmsMutability from "./functions/location-must-have-xms-mutability"
18+
import { lroAzureAsyncOperationHeader } from "./functions/lro-azure-async-operation-header"
1819
import validateOriginalUri from "./functions/lro-original-uri"
1920
import { lroPatch202 } from "./functions/lro-patch-202"
2021
import provisioningStateSpecifiedForLROPatch from "./functions/lro-patch-provisioning-state-specified"
@@ -158,6 +159,19 @@ const ruleset: any = {
158159
},
159160
},
160161

162+
// RPC Code: RPC-Async-V1-06
163+
LroAzureAsyncOperationHeader: {
164+
rpcGuidelineCode: "RPC-Async-V1-06",
165+
description: "All long-running operations must include an `Azure-AsyncOperation` response header.",
166+
message: "{{description}}",
167+
severity: "error",
168+
formats: [oas2],
169+
given: ["$[paths,'x-ms-paths'].*.*[?(@property === 'x-ms-long-running-operation' && @ === true)]^.responses.*"],
170+
then: {
171+
function: lroAzureAsyncOperationHeader,
172+
},
173+
},
174+
161175
// RPC Code: RPC-Async-V1-07
162176
LroLocationHeader: {
163177
rpcGuidelineCode: "RPC-Async-V1-07",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export const lroAzureAsyncOperationHeader = (headers: any, _opts: any, ctx: any) => {
2+
if (!Object.keys(headers).includes("headers") || !Object.keys(headers.headers).includes("Azure-AsyncOperation")) {
3+
return [
4+
{
5+
message: "All long-running operations must include an `Azure-AsyncOperation` response header.",
6+
path: ctx.path.concat("headers"),
7+
},
8+
]
9+
}
10+
return []
11+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Spectral } from "@stoplight/spectral-core"
2+
import linterForRule from "./utils"
3+
4+
let linter: Spectral
5+
6+
beforeAll(async () => {
7+
linter = await linterForRule("LroAzureAsyncOperationHeader")
8+
return linter
9+
})
10+
11+
const ERROR_MESSAGE = "All long-running operations must include an `Azure-AsyncOperation` response header."
12+
13+
test("LroAzureAsyncOperationHeader should find no errors", () => {
14+
const myOpenApiDocument = {
15+
swagger: "2.0",
16+
paths: {
17+
"/foo1/operations": {
18+
get: {
19+
operationId: "foo_get",
20+
responses: {
21+
202: {
22+
description: "Accepted",
23+
// no header scenario and no x-ms-long-running-operation
24+
},
25+
},
26+
},
27+
post: {
28+
operationId: "foo_post",
29+
"x-ms-long-running-operation": true,
30+
responses: {
31+
202: {
32+
description: "Accepted",
33+
headers: {
34+
"Azure-AsyncOperation": {
35+
description: "The URL where the status of the asynchronous operation can be checked.",
36+
type: "string",
37+
},
38+
},
39+
},
40+
},
41+
},
42+
put: {
43+
operationId: "foo_put",
44+
"x-ms-long-running-operation": true,
45+
responses: {
46+
204: {
47+
description: "Accepted",
48+
headers: {
49+
"Azure-AsyncOperation": {
50+
description: "The URL where the status of the asynchronous operation can be checked.",
51+
type: "string",
52+
},
53+
},
54+
},
55+
},
56+
},
57+
},
58+
},
59+
}
60+
return linter.run(myOpenApiDocument).then((results) => {
61+
expect(results.length).toBe(0)
62+
})
63+
})
64+
65+
test("LroAzureAsyncOperationHeader should find errors with no Azure-AsyncOperation header", () => {
66+
const myOpenApiDocument = {
67+
swagger: "2.0",
68+
paths: {
69+
"/foo1/operations": {
70+
get: {
71+
operationId: "foo_get",
72+
"x-ms-long-running-operation": true,
73+
responses: {
74+
202: {
75+
description: "Accepted",
76+
headers: {
77+
Location: {
78+
description: "No Azure-AsyncOperation header",
79+
type: "string",
80+
},
81+
},
82+
},
83+
},
84+
},
85+
post: {
86+
operationId: "foo_post",
87+
"x-ms-long-running-operation": true,
88+
responses: {
89+
202: {
90+
description: "No header case",
91+
},
92+
},
93+
},
94+
put: {
95+
operationId: "foo_put",
96+
"x-ms-long-running-operation": true,
97+
responses: {
98+
202: {
99+
description: "Accepted",
100+
headers: {
101+
"azure-asyncOperation1": {
102+
description: "check the wrong wording",
103+
type: "string",
104+
},
105+
},
106+
},
107+
},
108+
},
109+
delete: {
110+
operationId: "foo_delete",
111+
"x-ms-long-running-operation": true,
112+
responses: {
113+
202: {
114+
description: "Accepted",
115+
headers: {
116+
"azure-asyncOperation": {
117+
description: "check the camel case",
118+
type: "string",
119+
},
120+
},
121+
},
122+
},
123+
},
124+
},
125+
},
126+
}
127+
return linter.run(myOpenApiDocument).then((results) => {
128+
expect(results.length).toBe(4)
129+
expect(results[0].path.join(".")).toBe("paths./foo1/operations.get.responses.202.headers")
130+
expect(results[0].message).toEqual(ERROR_MESSAGE)
131+
expect(results[1].path.join(".")).toBe("paths./foo1/operations.post.responses.202")
132+
expect(results[1].message).toEqual(ERROR_MESSAGE)
133+
expect(results[2].path.join(".")).toBe("paths./foo1/operations.put.responses.202.headers")
134+
expect(results[2].message).toEqual(ERROR_MESSAGE)
135+
expect(results[3].path.join(".")).toBe("paths./foo1/operations.delete.responses.202.headers")
136+
expect(results[3].message).toEqual(ERROR_MESSAGE)
137+
})
138+
})

0 commit comments

Comments
 (0)