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

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""" 

7 

8from dateutil.parser import parse as parse_date 

9from dateutil.tz import tzlocal, tzutc 

10 

11from c7n.exceptions import PolicyValidationError, ClientError 

12from c7n.filters import Filter 

13from c7n.manager import resources 

14from c7n.utils import local_session, type_schema 

15 

16try: 

17 import jsonpatch 

18 HAVE_JSONPATH = True 

19except ImportError: 

20 HAVE_JSONPATH = False 

21 

22 

23ErrNotFound = "ResourceNotDiscoveredException" 

24 

25UTC = tzutc() 

26 

27 

28class Diff(Filter): 

29 """Compute the diff from the current resource to a previous version. 

30 

31 A resource matches the filter if a diff exists between the current 

32 resource and the selected revision. 

33 

34 Utilizes config as a resource revision database. 

35 

36 Revisions can be selected by date, against the previous version, and 

37 against a locked version (requires use of is-locked filter). 

38 """ 

39 

40 schema = type_schema( 

41 'diff', 

42 selector={'enum': ['previous', 'date', 'locked']}, 

43 # For date selectors allow value specification 

44 selector_value={'type': 'string'}) 

45 

46 permissions = ('config:GetResourceConfigHistory',) 

47 

48 selector_value = mode = parser = resource_shape = None 

49 

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)) 

61 

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 

75 

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() 

80 

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 

92 

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 

111 

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 

127 

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)} 

140 

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) 

145 

146 def diff(self, source, target): 

147 raise NotImplementedError("Subclass responsibility") 

148 

149 

150class JsonDiff(Diff): 

151 

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'}) 

157 

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) 

163 

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 

169 

170 @classmethod 

171 def register_resources(klass, registry, resource_class): 

172 """ meta model subscriber on resource registration. 

173 

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) 

180 

181 

182if HAVE_JSONPATH: 

183 resources.subscribe(JsonDiff.register_resources)