-
Notifications
You must be signed in to change notification settings - Fork 51
Expand file tree
/
Copy patharm-resource-validation.ts
More file actions
223 lines (211 loc) · 9.09 KB
/
arm-resource-validation.ts
File metadata and controls
223 lines (211 loc) · 9.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import { RuleContext } from "@microsoft.azure/openapi-validator-core"
import _ from "lodash"
import { ArmHelper } from "../utilities/arm-helper"
import { getResourceProvider } from "../utilities/rules-helper"
export function* trackedResourcesMustHavePut(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const allTrackedResources = armHelper.getTrackedResources()
for (const re of allTrackedResources) {
if (!re.operations.some((op: any) => op.httpMethod === "put")) {
yield {
location: ["definitions", re.modelName],
message: `The tracked resource ${re.modelName} does not have a corresponding put operation.`,
}
}
}
}
export function* trackedResourceBeyondsThirdLevel(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const allTrackedResources = armHelper.getTrackedResources()
const regex = /^.*\/providers\/microsoft\.\w+(?:\/\w+\/(\w+|{\w+})){4,}/gi
for (const re of allTrackedResources) {
if (re.operations.some((op: any) => regex.test(op.apiPath))) {
yield {
location: ["definitions", re.modelName],
message: `The tracked resource ${re.modelName} is beyond third level of nesting.`,
}
}
}
}
// support delete operation for all tracked resource , and all top level proxy resources.
export function* allResourcesHaveDelete(openapiSection: any, options: { isTrackedResource: boolean }, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const allResources = options.isTrackedResource ? armHelper.getTrackedResources() : armHelper.getProxyResources()
for (const re of allResources) {
const apiPath = re.operations.find((op: any) => op.apiPath)?.apiPath
if (apiPath && !apiPath.toLowerCase().includes("privateendpointconnectionproxies")) {
if ((options.isTrackedResource || armHelper.findOperation(apiPath, "put")) && !armHelper.findOperation(apiPath, "delete")) {
yield {
location: ["definitions", re.modelName],
message: `The resource ${re.modelName} does not have a corresponding delete operation.`,
}
}
}
}
}
// support delete operation for all tracked resource , and all top level proxy resources.
export function* trackedResourcesHavePatch(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const allTrackedResources = armHelper.getTrackedResources()
for (const re of allTrackedResources) {
const apiPath = re.operations.find((op: any) => op.apiPath)?.apiPath
if (apiPath && !apiPath.toLowerCase().includes("privateendpointconnectionproxies")) {
if (!armHelper.findOperation(apiPath, "patch")) {
yield {
location: ["definitions", re.modelName],
message: `Tracked resource '${re.modelName}' must have patch operation that at least supports the update of tags.`,
}
}
}
}
}
export function* armResourcePropertiesBag(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
// List of top-level properties to track
const propertiesBagTracked = [
"name",
"type",
"id",
"location",
"tags",
"plan",
"sku",
"etag",
"managedby",
"managedbyextended",
"identity",
"kind",
"zones",
"systemdata",
"extendedlocation",
]
// Get tracked resources from ARM helper
const trackedResources = armHelper.getTrackedResources()
// Process tracked resources with the tracked properties bag
yield* processResources(armHelper, trackedResources, propertiesBagTracked)
// List of proxy properties to track
const propertiesBagProxy = ["name", "id", "type", "etag", "systemdata"]
// Get proxy resources from ARM helper
const proxyResources = armHelper.getProxyResources()
// Process proxy resources with the proxy properties bag
yield* processResources(armHelper, proxyResources, propertiesBagProxy)
}
function checkPropertiesBag(armHelper: ArmHelper, model: any, resourceName: string, propertiesBag: string[], propertiesPath: string[]) {
let messages: any[] = []
const properties = armHelper.getProperty(model!, "properties")
if (properties) {
propertiesPath.push("properties")
for (const p of propertiesBag) {
if (armHelper.getProperty(properties, p)) {
messages.push(
`Top level property names should not be repeated inside the properties bag for ARM resource '${resourceName}'. Properties [${propertiesPath
.concat(p)
.join(".")}] conflict with ARM top level properties. Please rename these.`,
)
}
}
const subResult = checkPropertiesBag(armHelper, properties, resourceName, propertiesBag, propertiesPath)
messages = messages.concat(subResult)
}
return messages
}
function* processResources(armHelper: ArmHelper, resources: any[], propertiesBag: string[]) {
for (const resource of resources) {
const model = armHelper.getResourceByName(resource.modelName)
const messages = checkPropertiesBag(armHelper, model, resource.modelName, propertiesBag, [])
for (const message of messages) {
yield {
location: ["definitions", resource.modelName],
message,
}
}
}
}
export function* bodyTopLevelProperties(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const allResources = armHelper.getAllResources(true, false)
for (const re of allResources) {
const allowedBodyTopLevelProperties = [
"name",
"type",
"id",
"location",
"properties",
"tags",
"plan",
"sku",
"etag",
"managedby",
"managedbyextended",
"identity",
"kind",
"zones",
"systemdata",
"extendedlocation",
]
const properties = armHelper.getResourceProperties(re.modelName)
for (const propName of Object.keys(properties)) {
if (!allowedBodyTopLevelProperties.includes(propName.toLowerCase())) {
yield {
location: ["definitions", re.modelName],
message: `Top level properties should be one of name, type, id, location, properties, tags, plan, sku, etag, managedBy, managedByExtended, identity, kind, zones, systemData, extendedLocation. Model definition '${re.modelName}' has extra properties ['${propName}'].`,
}
}
}
}
}
export function* operationsAPIImplementation(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const operationsList = armHelper.getOperationApi()
if (!operationsList) {
const resourceProvider = getResourceProvider(ctx?.inventory!)
yield {
location: [],
message: `Operations API must be implemented for '${resourceProvider}'.`,
}
}
}
export function* resourcesHaveRequiredProperties(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const allResources = armHelper.getAllResources(true, false, true)
for (const re of allResources) {
const requiredProperties = ["name", "type", "id"]
const properties = armHelper.getResourceProperties(re.modelName)
// skip for common-types
if (properties["common-types"]) {
continue
}
for (const propName of requiredProperties) {
const prop = properties[propName]
if (!prop || armHelper.getAttribute(prop, "readOnly")?.value !== true) {
yield {
location: ["definitions", re.modelName],
message: `Model definition '${re.modelName}' must have the properties 'name', 'id' and 'type' in its hierarchy and these properties must be marked as readonly.`,
}
break
}
}
}
}
export function* xmsPageableListByRGAndSubscriptions(openapiSection: any, options: {}, ctx: RuleContext) {
const armHelper = new ArmHelper(ctx?.document, ctx?.specPath, ctx?.inventory!)
const trackedResources = armHelper.getTrackedResources()
const collectionApiInfos = armHelper.getCollectionApiInfo()
function isListByRgAndSubscription(apiPaths: string[]) {
return apiPaths.some((p) => armHelper.isPathByResourceGroup(p)) && apiPaths.some((p) => armHelper.isPathBySubscription(p))
}
for (const collectionApiInfo of collectionApiInfos) {
if (isListByRgAndSubscription(collectionApiInfo.collectionGetPath)) {
const trackedResource = trackedResources.find((r) => r.modelName === collectionApiInfo.childModelName)
const isXmsPageableResult = collectionApiInfo.collectionGetPath
.map((p) => armHelper.isPathXmsPageable(p))
.reduce((result, cur) => (result !== cur ? false : true))
if (trackedResource && !isXmsPageableResult) {
yield {
location: ["definitions", trackedResource.modelName],
message: `For the tracked resource '${trackedResource.modelName}', the x-ms-pageable extension values must be same for list by resource group and subscriptions operations.`,
}
}
}
}
}