Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/filters/revisions.py: 33%
107 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
3"""
4Custodian support for diffing and patching across multiple versions
5of a resource.
6"""
8from dateutil.parser import parse as parse_date
9from dateutil.tz import tzlocal, tzutc
11from c7n.exceptions import PolicyValidationError, ClientError
12from c7n.filters import Filter
13from c7n.manager import resources
14from c7n.utils import local_session, type_schema
16try:
17 import jsonpatch
18 HAVE_JSONPATH = True
19except ImportError:
20 HAVE_JSONPATH = False
23ErrNotFound = "ResourceNotDiscoveredException"
25UTC = tzutc()
28class Diff(Filter):
29 """Compute the diff from the current resource to a previous version.
31 A resource matches the filter if a diff exists between the current
32 resource and the selected revision.
34 Utilizes config as a resource revision database.
36 Revisions can be selected by date, against the previous version, and
37 against a locked version (requires use of is-locked filter).
38 """
40 schema = type_schema(
41 'diff',
42 selector={'enum': ['previous', 'date', 'locked']},
43 # For date selectors allow value specification
44 selector_value={'type': 'string'})
46 permissions = ('config:GetResourceConfigHistory',)
48 selector_value = mode = parser = resource_shape = None
50 def validate(self):
51 if 'selector' in self.data and self.data['selector'] == 'date':
52 if 'selector_value' not in self.data:
53 raise PolicyValidationError(
54 "Date version selector requires specification of date on %s" % (
55 self.manager.data))
56 try:
57 parse_date(self.data['selector_value'])
58 except ValueError:
59 raise PolicyValidationError(
60 "Invalid date for selector_value on %s" % (self.manager.data))
62 elif 'selector' in self.data and self.data['selector'] == 'locked':
63 idx = self.manager.data['filters'].index(self.data)
64 found = False
65 for n in self.manager.data['filters'][:idx]:
66 if isinstance(n, dict) and n.get('type', '') == 'locked':
67 found = True
68 if isinstance(n, str) and n == 'locked':
69 found = True
70 if not found:
71 raise PolicyValidationError(
72 "locked selector needs previous use of is-locked filter on %s" % (
73 self.manager.data))
74 return self
76 def process(self, resources, event=None):
77 session = local_session(self.manager.session_factory)
78 config = session.client('config')
79 self.model = self.manager.get_model()
81 results = []
82 for r in resources:
83 revisions = self.get_revisions(config, r)
84 r['c7n:previous-revision'] = rev = self.select_revision(revisions)
85 if not rev:
86 continue
87 delta = self.diff(rev['resource'], r)
88 if delta:
89 r['c7n:diff'] = delta
90 results.append(r)
91 return results
93 def get_revisions(self, config, resource):
94 params = dict(
95 resourceType=self.model.config_type,
96 resourceId=resource[self.model.id])
97 params.update(self.get_selector_params(resource))
98 try:
99 revisions = config.get_resource_config_history(
100 **params)['configurationItems']
101 except ClientError as e:
102 if e.response['Error']['Code'] == 'ResourceNotDiscoveredException':
103 return []
104 if e.response['Error']['Code'] != ErrNotFound:
105 self.log.debug(
106 "config - resource %s:%s not found" % (
107 self.model.config_type, resource[self.model.id]))
108 revisions = []
109 raise
110 return revisions
112 def get_selector_params(self, resource):
113 params = {}
114 selector = self.data.get('selector', 'previous')
115 if selector == 'date':
116 if not self.selector_value:
117 self.selector_value = parse_date(
118 self.data.get('selector_value'))
119 params['laterTime'] = self.selector_value
120 params['limit'] = 3
121 elif selector == 'previous':
122 params['limit'] = 2
123 elif selector == 'locked':
124 params['laterTime'] = resource.get('c7n:locked_date')
125 params['limit'] = 2
126 return params
128 def select_revision(self, revisions):
129 for rev in revisions:
130 # convert unix timestamp to utc to be normalized with other dates
131 if rev['configurationItemCaptureTime'].tzinfo and \
132 isinstance(rev['configurationItemCaptureTime'].tzinfo, tzlocal):
133 rev['configurationItemCaptureTime'] = rev[
134 'configurationItemCaptureTime'].astimezone(UTC)
135 return {
136 'date': rev['configurationItemCaptureTime'],
137 'version_id': rev['configurationStateId'],
138 'events': rev['relatedEvents'],
139 'resource': self.transform_revision(rev)}
141 def transform_revision(self, revision):
142 """make config revision look like describe output."""
143 config = self.manager.get_source('config')
144 return config.load_resource(revision)
146 def diff(self, source, target):
147 raise NotImplementedError("Subclass responsibility")
150class JsonDiff(Diff):
152 schema = type_schema(
153 'json-diff',
154 selector={'enum': ['previous', 'date', 'locked']},
155 # For date selectors allow value specification
156 selector_value={'type': 'string'})
158 def diff(self, source, target):
159 source, target = (
160 self.sanitize_revision(source), self.sanitize_revision(target))
161 patch = jsonpatch.JsonPatch.from_diff(source, target)
162 return list(patch)
164 def sanitize_revision(self, rev):
165 sanitized = dict(rev)
166 for k in [k for k in sanitized if 'c7n' in k]:
167 sanitized.pop(k)
168 return sanitized
170 @classmethod
171 def register_resources(klass, registry, resource_class):
172 """ meta model subscriber on resource registration.
174 We watch for new resource types being registered and if they
175 support aws config, automatically register the jsondiff filter.
176 """
177 if resource_class.resource_type.config_type is None:
178 return
179 resource_class.filter_registry.register('json-diff', klass)
182if HAVE_JSONPATH:
183 resources.subscribe(JsonDiff.register_resources)