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
« 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
4"""Deprecations of policy elements.
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
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')
18- mark -> tag
19- unmark, untag -> remove-tag
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')
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)
38"""
40# Treat deprecation warnings as errors.
41STRICT = "strict"
42# Skip checking for deprecations
43SKIP = "skip"
46def alias(name, removed_after=None, link=None):
47 """A filter or action alias is deprecated."""
48 return DeprecatedAlias(name, removed_after, link)
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)
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)
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)
66def optional_field(name, removed_after=None, link=None):
67 """The field must now be specified."""
68 return DeprecatedOptionality([name], removed_after, link)
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)
76class Deprecation:
77 """Base class for different deprecation types."""
78 _id = 0
80 def __init__(self, removed_after, link):
81 """All deprecations can have a removal date, and a link to docs.
83 Both of these fields are optional.
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
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}"
98 def check(self, data):
99 return True
102class DeprecatedAlias(Deprecation):
103 def __init__(self, name, removed_after=None, link=None):
104 super().__init__(removed_after, link)
105 self.name = name
107 def __str__(self):
108 return f"alias '{self.name}' has been deprecated"
110 def check(self, data):
111 return data.get("type") == self.name
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
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})"
129 def check(self, data):
130 if self.name in data:
131 return True
132 return False
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
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})"
146 def check(self, data):
147 return True
150class DeprecatedOptionality(Deprecation):
151 def __init__(self, fields, removed_after=None, link=None):
152 super().__init__(removed_after, link)
153 self.fields = fields
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])
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)"
165 field = self.fields[0]
166 return f"optional field '{field}' deprecated (must be specified)"
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}"
175class Context:
176 """Adds extra context to a deprecation."""
177 def __init__(self, context, deprecation):
178 self.context = context
179 self.deprecation = deprecation
181 def __str__(self):
182 return f"{self.context} {self.deprecation}"
184 @property
185 def id(self):
186 return self.deprecation.id
188 @property
189 def link(self):
190 return self.deprecation.link
192 @property
193 def remove_text(self):
194 return self.deprecation.remove_text
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
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)
226class Report:
227 """A deprecation report is generated per policy."""
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
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
255 def format(self, source_locator=None, footnotes=None):
256 """Format the report for output.
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)
275 def section(self, name, deprecations, footnotes):
276 count = len(deprecations)
277 if count == 0:
278 return ()
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
289class Footnotes:
290 """A helper for defining and listing footnotes for deprecations.
292 The deprecation date and URL being shown for every deprecation warning
293 during validation would make the output repetitive and ungainly.
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 = []
303 def note(self, d):
304 """Return a reference to the footnote if the deprecation has one.
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}]"
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
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)