Coverage for fake_kubernetes_client/resource_storage.py: 13%

119 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2025-07-29 12:31 +0300

1"""FakeResourceStorage implementation for fake Kubernetes client""" 

2 

3import copy 

4from collections import defaultdict 

5from typing import Any, DefaultDict, Union 

6 

7 

8class FakeResourceStorage: 

9 """In-memory storage for Kubernetes resources""" 

10 

11 def __init__(self) -> None: 

12 # Storage structure: {api_version: {kind: {namespace: {name: resource}}}} 

13 self.resources: DefaultDict[str, DefaultDict[str, DefaultDict[Union[str, None], dict[str, Any]]]] = defaultdict( 

14 lambda: defaultdict(lambda: defaultdict(dict)) 

15 ) 

16 

17 def store_resource( 

18 self, kind: str, api_version: str, name: str, namespace: Union[str, None], resource: dict[str, Any] 

19 ) -> None: 

20 """Store a resource""" 

21 self.resources[api_version][kind][namespace][name] = copy.deepcopy(resource) 

22 

23 def get_resource( 

24 self, kind: str, api_version: str, name: str, namespace: Union[str, None] 

25 ) -> Union[dict[str, Any], None]: 

26 """Get a specific resource""" 

27 api_resources = self.resources.get(api_version) 

28 if not api_resources: 

29 return None 

30 kind_resources = api_resources.get(kind) 

31 if not kind_resources: 

32 return None 

33 namespace_resources = kind_resources.get(namespace) 

34 if not namespace_resources: 

35 return None 

36 resource = namespace_resources.get(name) 

37 return copy.deepcopy(resource) if resource else None 

38 

39 def list_resources( 

40 self, 

41 kind: str, 

42 api_version: str, 

43 namespace: Union[str, None] = None, 

44 label_selector: Union[str, None] = None, 

45 field_selector: Union[str, None] = None, 

46 ) -> list[dict[str, Any]]: 

47 """List resources with optional filtering""" 

48 resources: list[dict[str, Any]] = [] 

49 

50 api_resources = self.resources.get(api_version) 

51 if not api_resources: 

52 return resources 

53 

54 kind_resources = api_resources.get(kind) 

55 if not kind_resources: 

56 return resources 

57 

58 if namespace is not None: 

59 # List resources in specific namespace 

60 namespace_resources = kind_resources.get(namespace, {}) 

61 resources = list(namespace_resources.values()) 

62 else: 

63 # List resources across all namespaces 

64 for ns_resources in kind_resources.values(): 

65 resources.extend(ns_resources.values()) 

66 

67 # Apply label selector filter 

68 if label_selector: 

69 resources = self._filter_by_labels(resources, label_selector) 

70 

71 # Apply field selector filter 

72 if field_selector: 

73 resources = self._filter_by_fields(resources, field_selector) 

74 

75 return [copy.deepcopy(r) for r in resources] 

76 

77 def delete_resource( 

78 self, kind: str, api_version: str, name: str, namespace: Union[str, None] 

79 ) -> Union[dict[str, Any], None]: 

80 """Delete a resource""" 

81 resource = self.get_resource(kind, api_version, name, namespace) 

82 if resource: 

83 del self.resources[api_version][kind][namespace][name] 

84 # Clean up empty structures 

85 if not self.resources[api_version][kind][namespace]: 

86 del self.resources[api_version][kind][namespace] 

87 if not self.resources[api_version][kind]: 

88 del self.resources[api_version][kind] 

89 if not self.resources[api_version]: 

90 del self.resources[api_version] 

91 return resource 

92 

93 def _filter_by_labels(self, resources: list[dict[str, Any]], label_selector: str) -> list[dict[str, Any]]: 

94 """Filter resources by label selector""" 

95 filtered = [] 

96 for resource in resources: 

97 labels = resource.get("metadata", {}).get("labels", {}) 

98 if self._matches_label_selector(labels, label_selector): 

99 filtered.append(resource) 

100 return filtered 

101 

102 def _matches_label_selector(self, labels: dict[str, str], selector: str) -> bool: 

103 """Check if labels match selector (simplified implementation)""" 

104 # Handle simple equality selectors (key=value) 

105 parts = selector.split(",") 

106 for part in parts: 

107 if "=" in part: 

108 key, value = part.split("=", 1) 

109 if labels.get(key.strip()) != value.strip(): 

110 return False 

111 elif "!=" in part: 

112 key, value = part.split("!=", 1) 

113 if labels.get(key.strip()) == value.strip(): 

114 return False 

115 else: 

116 # Just key presence check 

117 if part.strip() not in labels: 

118 return False 

119 return True 

120 

121 def _filter_by_fields(self, resources: list[dict[str, Any]], field_selector: str) -> list[dict[str, Any]]: 

122 """Filter resources by field selector""" 

123 filtered = [] 

124 for resource in resources: 

125 if self._matches_field_selector(resource, field_selector): 

126 filtered.append(resource) 

127 return filtered 

128 

129 def _matches_field_selector(self, resource: dict[str, Any], selector: str) -> bool: 

130 """Check if resource matches field selector""" 

131 # Handle simple field selectors (field.path=value or field.path==value) 

132 parts = selector.split(",") 

133 for part in parts: 

134 if "==" in part: 

135 # Handle double equals 

136 field_path, selector_value = part.split("==", 1) 

137 field_value = self._get_field_value(resource, field_path.strip()) 

138 if not self._compare_field_values(field_value, selector_value.strip()): 

139 return False 

140 elif "=" in part: 

141 # Handle single equals 

142 field_path, selector_value = part.split("=", 1) 

143 field_value = self._get_field_value(resource, field_path.strip()) 

144 if not self._compare_field_values(field_value, selector_value.strip()): 

145 return False 

146 return True 

147 

148 def _compare_field_values(self, field_value: Any, selector_value: str) -> bool: 

149 """ 

150 Compare field value with selector value using type-aware comparison. 

151 

152 Attempts to parse the selector value to match the field value's type. 

153 """ 

154 # Handle missing fields - they don't match any selector 

155 if field_value is NotImplemented: 

156 return False 

157 

158 # Handle None/null values 

159 if field_value is None: 

160 return selector_value.lower() in ("none", "null", "") 

161 

162 # Handle boolean values 

163 if isinstance(field_value, bool): 

164 if selector_value.lower() == "true": 

165 return field_value is True 

166 elif selector_value.lower() == "false": 

167 return field_value is False 

168 else: 

169 return False 

170 

171 # Handle numeric values 

172 if isinstance(field_value, (int, float)): 

173 try: 

174 # Try to parse as number 

175 if "." in selector_value: 

176 return field_value == float(selector_value) 

177 else: 

178 return field_value == int(selector_value) 

179 except ValueError: 

180 # If parsing fails, fall back to string comparison 

181 return str(field_value) == selector_value 

182 

183 # Handle string values (default case) 

184 return str(field_value) == selector_value 

185 

186 def _get_field_value(self, obj: dict[str, Any], path: str) -> Any: 

187 """Get value from nested dictionary using dot notation""" 

188 current = obj 

189 parts = path.split(".") 

190 

191 for i, part in enumerate(parts): 

192 if isinstance(current, dict): 

193 if part not in current: 

194 # Return a sentinel value to indicate the field doesn't exist 

195 return NotImplemented 

196 current = current[part] 

197 else: 

198 return NotImplemented 

199 

200 return current