networking_istio_io_v1alpha3_api.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. from enum import Enum
  2. from kubernetes_asyncio import client
  3. from kubernetes_asyncio.client import V1ObjectMeta
  4. from typing import List, Optional, Dict, Any
  5. from pydantic import BaseModel, Field
  6. # --- McpBridge Data Structures (Pydantic) ---
  7. GROUP = "networking.istio.io"
  8. VERSION = "v1alpha3"
  9. PLURAL = "envoyfilters"
  10. class ApplyToStringEnum(str, Enum):
  11. INVALID = "INVALID"
  12. LISTENER = "LISTENER"
  13. FILTER_CHAIN = "FILTER_CHAIN"
  14. NETWORK_FILTER = "NETWORK_FILTER"
  15. HTTP_FILTER = "HTTP_FILTER"
  16. ROUTE_CONFIGURATION = "ROUTE_CONFIGURATION"
  17. VIRTUAL_HOST = "VIRTUAL_HOST"
  18. HTTP_ROUTE = "HTTP_ROUTE"
  19. CLUSTER = "CLUSTER"
  20. EXTENSION_CONFIG = "EXTENSION_CONFIG"
  21. BOOTSTRAP = "BOOTSTRAP"
  22. LISTENER_FILTER = "LISTENER_FILTER"
  23. class MatchContextEnum(str, Enum):
  24. ANY = "ANY"
  25. SIDECAR_INBOUND = "SIDECAR_INBOUND"
  26. SIDECAR_OUTBOUND = "SIDECAR_OUTBOUND"
  27. GATEWAY = "GATEWAY"
  28. class OperationStringEnum(str, Enum):
  29. INVALID = "INVALID"
  30. MERGE = "MERGE"
  31. ADD = "ADD"
  32. REMOVE = "REMOVE"
  33. INSERT_BEFORE = "INSERT_BEFORE"
  34. INSERT_AFTER = "INSERT_AFTER"
  35. INSERT_FIRST = "INSERT_FIRST"
  36. REPLACE = "REPLACE"
  37. class PatchFilterClassEnum(str, Enum):
  38. UNSPECIFIED = "UNSPECIFIED"
  39. AUTHN = "AUTHN"
  40. AUTHZ = "AUTHZ"
  41. STATS = "STATS"
  42. class RouteActionEnum(str, Enum):
  43. ANY = "ANY"
  44. ROUTE = "ROUTE"
  45. REDIRECT = "REDIRECT"
  46. DIRECT_RESPONSE = "DIRECT_RESPONSE"
  47. class WorkloadSelector(BaseModel):
  48. labels: Optional[Dict[str, str]] = Field(default_factory=dict)
  49. class PolicyTargetReference(BaseModel):
  50. group: str
  51. kind: str
  52. name: str
  53. namespace: str = ""
  54. class ProxyMatch(BaseModel):
  55. proxyVersion: Optional[str] = None
  56. metadata: Optional[Dict[str, Any]] = None
  57. class ListenerMatch(BaseModel):
  58. portNumber: Optional[int] = None
  59. portName: Optional[str] = None
  60. filterChain: Optional[Dict[str, Any]] = None
  61. listenerFilter: Optional[str] = None
  62. name: Optional[str] = None
  63. class VirtualHostMatchRoute(BaseModel):
  64. name: Optional[str] = None
  65. action: Optional[RouteActionEnum] = None
  66. class VirtualHostMatch(BaseModel):
  67. name: Optional[str] = None
  68. domainName: Optional[str] = None
  69. route: Optional[VirtualHostMatchRoute] = None
  70. class RouteConfigurationMatch(BaseModel):
  71. portNumber: Optional[int] = None
  72. portName: Optional[str] = None
  73. gateway: Optional[str] = None
  74. vhost: Optional[VirtualHostMatch] = None
  75. name: Optional[str] = None
  76. class ClusterMatch(BaseModel):
  77. portNumber: Optional[int] = None
  78. service: Optional[str] = None
  79. subset: Optional[str] = None
  80. name: Optional[str] = None
  81. class MatchObjectType(BaseModel):
  82. listener: Optional[ListenerMatch] = None
  83. routeConfiguration: Optional[RouteConfigurationMatch] = None
  84. cluster: Optional[ClusterMatch] = None
  85. class EnvoyConfigObjectMatch(MatchObjectType):
  86. context: MatchContextEnum
  87. proxy: Optional[ProxyMatch] = None
  88. class Patch(BaseModel):
  89. operation: OperationStringEnum
  90. value: Optional[Dict[str, Any]] = None
  91. filterClass: Optional[PatchFilterClassEnum] = None
  92. class EnvoyConfigPatchObject(BaseModel):
  93. applyTo: ApplyToStringEnum
  94. match: Optional[EnvoyConfigObjectMatch] = None
  95. patch: Optional[Patch] = None
  96. class EnvoyFilterSpec(BaseModel):
  97. workloadSelector: Optional[WorkloadSelector] = None
  98. targetRefs: Optional[List[PolicyTargetReference]] = None
  99. configPatches: Optional[List[EnvoyConfigPatchObject]] = None
  100. priority: Optional[int] = None
  101. class EnvoyFilter(BaseModel):
  102. apiVersion: str = f"{GROUP}/{VERSION}"
  103. kind: str = "EnvoyFilter"
  104. metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
  105. spec: Optional[EnvoyFilterSpec] = None
  106. def parse_metadata(self) -> V1ObjectMeta:
  107. """
  108. Parse the metadata dictionary into a V1ObjectMeta object.
  109. """
  110. return V1ObjectMeta(**self.metadata) if self.metadata else V1ObjectMeta()
  111. class NetworkingIstioIoV1Alpha3Api:
  112. def __init__(self, api_client: client.ApiClient):
  113. self.custom_api = client.CustomObjectsApi(api_client)
  114. async def edit_envoyfilter(
  115. self, namespace: str, name: str, body: EnvoyFilter
  116. ) -> Dict[str, Any]:
  117. """Edit (replace) a EnvoyFilter resource."""
  118. return await self.custom_api.replace_namespaced_custom_object(
  119. GROUP,
  120. VERSION,
  121. namespace,
  122. PLURAL,
  123. name,
  124. (
  125. body.model_dump(by_alias=True, exclude_none=True)
  126. if isinstance(body, EnvoyFilter)
  127. else body
  128. ),
  129. )
  130. async def create_envoyfilter(
  131. self, namespace: str, body: EnvoyFilter
  132. ) -> Dict[str, Any]:
  133. """Create a EnvoyFilter resource in the given namespace."""
  134. return await self.custom_api.create_namespaced_custom_object(
  135. GROUP,
  136. VERSION,
  137. namespace,
  138. PLURAL,
  139. (
  140. body.model_dump(by_alias=True, exclude_none=True)
  141. if isinstance(body, EnvoyFilter)
  142. else body
  143. ),
  144. )
  145. async def get_envoyfilter(self, namespace: str, name: str) -> Dict[str, Any]:
  146. """Get a EnvoyFilter resource by name."""
  147. return await self.custom_api.get_namespaced_custom_object(
  148. GROUP, VERSION, namespace, PLURAL, name
  149. )
  150. async def list_envoyfilters(
  151. self, namespace: str, label_selector: Optional[str] = None
  152. ) -> Dict[str, Any]:
  153. """List all EnvoyFilter resources in the given namespace."""
  154. return await self.custom_api.list_namespaced_custom_object(
  155. GROUP, VERSION, namespace, PLURAL, label_selector=label_selector
  156. )
  157. async def patch_envoyfilter(
  158. self, namespace: str, name: str, body: Dict[str, Any]
  159. ) -> Dict[str, Any]:
  160. """Patch a EnvoyFilter resource."""
  161. return await self.custom_api.patch_namespaced_custom_object(
  162. GROUP, VERSION, namespace, PLURAL, name, body
  163. )
  164. async def delete_envoyfilter(
  165. self, namespace: str, name: str, body: Optional[Dict[str, Any]] = None
  166. ) -> Dict[str, Any]:
  167. """Delete a EnvoyFilter resource."""
  168. return await self.custom_api.delete_namespaced_custom_object(
  169. GROUP, VERSION, namespace, PLURAL, name, body=body
  170. )
  171. def get_4xx_5xx_fallback_value(
  172. ingress_name: str,
  173. fallback_header: str = "x-higress-fallback-from",
  174. extra_req_headers: Optional[Dict[str, str]] = None,
  175. ) -> Dict[str, Any]:
  176. response_headers = [
  177. {
  178. "append": False,
  179. "header": {"key": fallback_header, "value": ingress_name},
  180. }
  181. ]
  182. request_headers = response_headers.copy()
  183. if extra_req_headers:
  184. for key, value in extra_req_headers.items():
  185. request_headers.append(
  186. {
  187. "append": False,
  188. "header": {"key": key, "value": value},
  189. }
  190. )
  191. redirect_policy = {
  192. "keep_original_response_code": False,
  193. "max_internal_redirects": 10,
  194. "only_redirect_upstream_code": False,
  195. "request_headers_to_add": request_headers,
  196. "response_headers_to_add": response_headers,
  197. "use_original_request_body": True,
  198. "use_original_request_uri": True,
  199. }
  200. action = {
  201. "name": "action",
  202. "typed_config": {
  203. "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
  204. "type_url": "type.googleapis.com/envoy.extensions.http.custom_response.redirect_policy.v3.RedirectPolicy",
  205. "value": redirect_policy,
  206. },
  207. }
  208. def predicate_response_code(code_class: str) -> Dict[str, Any]:
  209. return {
  210. "single_predicate": {
  211. "input": {
  212. "name": f"{code_class}_response",
  213. "typed_config": {
  214. "@type": "type.googleapis.com/envoy.type.matcher.v3.HttpResponseStatusCodeClassMatchInput"
  215. },
  216. },
  217. "value_match": {"exact": code_class},
  218. }
  219. }
  220. # matcher
  221. matcher = {
  222. "on_match": {"action": action},
  223. "predicate": {
  224. "or_matcher": {
  225. "predicate": [
  226. predicate_response_code("4xx"),
  227. predicate_response_code("5xx"),
  228. ]
  229. }
  230. },
  231. }
  232. # custom_response_matcher
  233. custom_response_matcher = {"matcher_list": {"matchers": [matcher]}}
  234. typed_per_filter_config = {
  235. "typed_per_filter_config": {
  236. "envoy.filters.http.custom_response": {
  237. "@type": "type.googleapis.com/udpa.type.v1.TypedStruct",
  238. "type_url": "type.googleapis.com/envoy.extensions.filters.http.custom_response.v3.CustomResponse",
  239. "value": {"custom_response_matcher": custom_response_matcher},
  240. }
  241. }
  242. }
  243. return typed_per_filter_config
  244. def get_ingress_fallback_envoyfilter(
  245. ingress_name: str,
  246. namespace: str,
  247. fallback_header: str = "x-higress-fallback-from",
  248. labels: Optional[Dict[str, str]] = None,
  249. extra_req_headers: Optional[Dict[str, str]] = None,
  250. ) -> EnvoyFilter:
  251. object_match = EnvoyConfigObjectMatch(
  252. context=MatchContextEnum.GATEWAY,
  253. routeConfiguration=RouteConfigurationMatch(
  254. vhost=VirtualHostMatch(
  255. route=VirtualHostMatchRoute(
  256. name=ingress_name,
  257. ),
  258. ),
  259. ),
  260. )
  261. envoyfilter = EnvoyFilter(
  262. metadata={
  263. "name": ingress_name,
  264. "namespace": namespace,
  265. "labels": labels or {},
  266. },
  267. spec=EnvoyFilterSpec(
  268. configPatches=[
  269. EnvoyConfigPatchObject(
  270. applyTo=ApplyToStringEnum.HTTP_ROUTE,
  271. match=object_match,
  272. patch=Patch(
  273. operation=OperationStringEnum.MERGE,
  274. value=get_4xx_5xx_fallback_value(
  275. ingress_name, fallback_header, extra_req_headers
  276. ),
  277. ),
  278. )
  279. ]
  280. ),
  281. )
  282. return envoyfilter