Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/deprecated.py: 36%

200 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-08 06:51 +0000

1# Copyright The Cloud Custodian Authors. 

2# SPDX-License-Identifier: Apache-2.0 

3 

4"""Deprecations of policy elements. 

5 

6Initial thinking around the deprecation is identifying changes in filters and 

7actions. These are likely to be the most common aspects: 

8 * renaming a field 

9 * making an optional field required 

10 * removing a field 

11 

12Examples: 

13 - renaming a filter itself 

14c7n_azure/resources/key_vault 

15@KeyVault.filter_registry.register('whitelist') 

16- filter 'whitelist' has been deprecated (replaced by 'allow') 

17 

18- mark -> tag 

19- unmark, untag -> remove-tag 

20 

21 

22 - renaming filter attributes 

23c7n/filters/iamaccess 

24 schema = type_schema( 

25 'cross-account', 

26 ... 

27 whitelist_from={'$ref': '#/definitions/filters_common/value_from'}, 

28 whitelist={'type': 'array', 'items': {'type': 'string'}}, 

29 ...) 

30- filter field 'whitelist' has been deprecated (replaced by 'allow') 

31 

32 

33c7n/tags.py 

34 - optional attributes becoming required, in this case one of 'days' or 'hours' 

35 - optional action fields deprecated (one of 'days' or 'hours' must be specified) 

36 - optional action field 'tag' deprecated (must be specified) 

37 

38""" 

39 

40# Treat deprecation warnings as errors. 

41STRICT = "strict" 

42# Skip checking for deprecations 

43SKIP = "skip" 

44 

45 

46def alias(name, removed_after=None, link=None): 

47 """A filter or action alias is deprecated.""" 

48 return DeprecatedAlias(name, removed_after, link) 

49 

50 

51def action(replacement, removed_after=None, link=None): 

52 """The action has been superseded by another action.""" 

53 return DeprecatedElement('action', replacement, removed_after, link) 

54 

55 

56def filter(replacement, removed_after=None, link=None): 

57 """The filter has been superseded by another filter.""" 

58 return DeprecatedElement('filter', replacement, removed_after, link) 

59 

60 

61def field(name, replacement, removed_after=None, link=None): 

62 """The field has been renamed to something else.""" 

63 return DeprecatedField(name, replacement, removed_after, link) 

64 

65 

66def optional_field(name, removed_after=None, link=None): 

67 """The field must now be specified.""" 

68 return DeprecatedOptionality([name], removed_after, link) 

69 

70 

71def optional_fields(names, removed_after=None, link=None): 

72 """One of the field names must now be specified.""" 

73 return DeprecatedOptionality(names, removed_after, link) 

74 

75 

76class Deprecation: 

77 """Base class for different deprecation types.""" 

78 _id = 0 

79 

80 def __init__(self, removed_after, link): 

81 """All deprecations can have a removal date, and a link to docs. 

82 

83 Both of these fields are optional. 

84 

85 removed_after if specified must be a string representing an ISO8601 date. 

86 """ 

87 self.removed_after = removed_after 

88 self.link = link 

89 self.id = Deprecation._id 

90 Deprecation._id += 1 

91 

92 @property 

93 def remove_text(self): 

94 if self.removed_after is None: 

95 return "" 

96 return f"Will be removed after {self.removed_after}" 

97 

98 def check(self, data): 

99 return True 

100 

101 

102class DeprecatedAlias(Deprecation): 

103 def __init__(self, name, removed_after=None, link=None): 

104 super().__init__(removed_after, link) 

105 self.name = name 

106 

107 def __str__(self): 

108 return f"alias '{self.name}' has been deprecated" 

109 

110 def check(self, data): 

111 return data.get("type") == self.name 

112 

113 

114class DeprecatedField(Deprecation): 

115 def __init__(self, name, replacement, removed_after=None, link=None): 

116 super().__init__(removed_after, link) 

117 self.name = name 

118 self.replacement = replacement 

119 

120 def __str__(self): 

121 # filter or action prefix added by Filter and Action classes 

122 name, replacement = self.name, self.replacement 

123 # If the replacement is a single word we surround it with quotes, 

124 # otherwise we just use the replacement as is. 

125 if ' ' not in replacement: 

126 replacement = "'" + replacement + "'" 

127 return f"field '{name}' has been deprecated (replaced by {replacement})" 

128 

129 def check(self, data): 

130 if self.name in data: 

131 return True 

132 return False 

133 

134 

135class DeprecatedElement(Deprecation): 

136 def __init__(self, name, replacement, removed_after=None, link=None): 

137 super().__init__(removed_after, link) 

138 self.name = name 

139 self.replacement = replacement 

140 

141 def __str__(self): 

142 # filter or action prefix added by Filter and Action classes 

143 name, replacement = self.name, self.replacement 

144 return f"{name} has been deprecated ({replacement})" 

145 

146 def check(self, data): 

147 return True 

148 

149 

150class DeprecatedOptionality(Deprecation): 

151 def __init__(self, fields, removed_after=None, link=None): 

152 super().__init__(removed_after, link) 

153 self.fields = fields 

154 

155 def check(self, data): 

156 # check to see that they haven't specified the value 

157 return all([key not in data for key in self.fields]) 

158 

159 def __str__(self): 

160 if len(self.fields) > 1: 

161 quoted = [f"'{field}'" for field in self.fields] 

162 names = ' or '.join(quoted) 

163 return f"optional fields deprecated (one of {names} must be specified)" 

164 

165 field = self.fields[0] 

166 return f"optional field '{field}' deprecated (must be specified)" 

167 

168 @property 

169 def remove_text(self): 

170 if self.removed_after is None: 

171 return "" 

172 return f"Will become an error after {self.removed_after}" 

173 

174 

175class Context: 

176 """Adds extra context to a deprecation.""" 

177 def __init__(self, context, deprecation): 

178 self.context = context 

179 self.deprecation = deprecation 

180 

181 def __str__(self): 

182 return f"{self.context} {self.deprecation}" 

183 

184 @property 

185 def id(self): 

186 return self.deprecation.id 

187 

188 @property 

189 def link(self): 

190 return self.deprecation.link 

191 

192 @property 

193 def remove_text(self): 

194 return self.deprecation.remove_text 

195 

196 

197def check_deprecations(source, context=None, data=None): 

198 if data is None: 

199 data = getattr(source, 'data', {}) 

200 deprecations = [] 

201 for d in getattr(source, 'deprecations', ()): 

202 if d.check(data): 

203 if context is not None: 

204 d = Context(context, d) 

205 deprecations.append(d) 

206 return deprecations 

207 

208 

209def report(policy): 

210 """Generate the deprecation report for the policy.""" 

211 policy_fields = policy.get_deprecations() 

212 conditions = policy.conditions.get_deprecations() 

213 mode = policy.get_execution_mode().get_deprecations() 

214 filters = [] 

215 actions = [] 

216 rm = policy.resource_manager 

217 resource = rm.get_deprecations() 

218 for f in rm.filters: 

219 filters.extend(f.get_deprecations()) 

220 for a in rm.actions: 

221 actions.extend(a.get_deprecations()) 

222 return Report(policy.name, policy_fields, conditions, 

223 mode, resource, filters, actions) 

224 

225 

226class Report: 

227 """A deprecation report is generated per policy.""" 

228 

229 def __init__(self, policy_name, policy_fields=(), conditions=(), mode=(), 

230 resource=(), filters=(), actions=()): 

231 self.policy_name = policy_name 

232 self.policy_fields = policy_fields 

233 self.conditions = conditions 

234 self.mode = mode 

235 self.resource = resource 

236 self.filters = filters 

237 self.actions = actions 

238 

239 def __bool__(self): 

240 # Start by checking the most likely things. 

241 if len(self.filters) > 0: 

242 return True 

243 if len(self.actions) > 0: 

244 return True 

245 if len(self.policy_fields) > 0: 

246 return True 

247 if len(self.conditions) > 0: 

248 return True 

249 if len(self.resource) > 0: 

250 return True 

251 if len(self.mode) > 0: 

252 return True 

253 return False 

254 

255 def format(self, source_locator=None, footnotes=None): 

256 """Format the report for output. 

257 

258 If a source locator is specified, it is used to provide file and line number 

259 information for the policy. 

260 """ 

261 location = "" 

262 if source_locator is not None: 

263 file_and_line = source_locator.find(self.policy_name) 

264 if file_and_line: 

265 location = f" ({file_and_line})" 

266 lines = [f"policy '{self.policy_name}'{location}"] 

267 lines.extend(self.section('attributes', self.policy_fields, footnotes)) 

268 lines.extend(self.section('condition', self.conditions, footnotes)) 

269 lines.extend(self.section('mode', self.mode, footnotes)) 

270 lines.extend(self.section('resource', self.resource, footnotes)) 

271 lines.extend(self.section('filters', self.filters, footnotes)) 

272 lines.extend(self.section('actions', self.actions, footnotes)) 

273 return "\n".join(lines) 

274 

275 def section(self, name, deprecations, footnotes): 

276 count = len(deprecations) 

277 if count == 0: 

278 return () 

279 

280 def footnote(d): 

281 if footnotes is None: 

282 return "" 

283 return footnotes.note(d) 

284 result = [f" {name}:"] 

285 result.extend([f" {d}{footnote(d)}" for d in deprecations]) 

286 return result 

287 

288 

289class Footnotes: 

290 """A helper for defining and listing footnotes for deprecations. 

291 

292 The deprecation date and URL being shown for every deprecation warning 

293 during validation would make the output repetitive and ungainly. 

294 

295 This mechanism can allow for a note to be added at the end of each 

296 deprecation line and have the dates and URLs if they exist, shown at the 

297 end. 

298 """ 

299 def __init__(self): 

300 self.seen = {} 

301 self.notes = [] 

302 

303 def note(self, d): 

304 """Return a reference to the footnote if the deprecation has one. 

305 

306 A deprecation will have a footnote if either the remove_date or the URL are set. 

307 """ 

308 if d.id not in self.seen: 

309 footnote = self._note(d) 

310 if not footnote: 

311 self.seen[d.id] = None 

312 return "" 

313 self.notes.append(footnote) 

314 ref = len(self.notes) 

315 self.seen[d.id] = ref 

316 else: 

317 ref = self.seen[d.id] 

318 if ref is None: 

319 return "" 

320 return f" [{ref}]" 

321 

322 def _note(self, d): 

323 removed = d.remove_text 

324 if not removed and d.link is None: 

325 return "" 

326 text = "" 

327 if d.link is not None: 

328 text = f"See {d.link}" 

329 if removed: 

330 text += ", " 

331 removed = removed[0].lower() + removed[1:] 

332 if removed: 

333 text += removed 

334 return text 

335 

336 def __call__(self): 

337 lines = [] 

338 for i, note in enumerate(self.notes, 1): 

339 lines.append(f"[{i}] {note}") 

340 return "\n".join(lines)