3535# Configure logging
3636logger = logging .getLogger ()
3737logHandler = logging .StreamHandler (sys .stdout )
38- formatter = logging .Formatter (' %(asctime)s - %(levelname)s - %(message)s' )
38+ formatter = logging .Formatter (" %(asctime)s - %(levelname)s - %(message)s" )
3939logHandler .setFormatter (formatter )
4040logger .addHandler (logHandler )
4141logger .setLevel (os .environ .get ("LOG_LEVEL" , "INFO" ))
4242
4343# Define the mention pattern regex
44- MENTION_PATTERN = re .compile (r' @[\w.-]+' )
44+ MENTION_PATTERN = re .compile (r" @[\w.-]+" )
4545ENFORCE = "enforce"
4646IGNORE = "ignore"
4747
4848app = FastAPI (
4949 title = "KRR Enforcer mutation webhook" ,
5050 description = "A KRR recommendations mutating webhook server for Kubernetes" ,
51- version = "1.0.0"
51+ version = "1.0.0" ,
5252)
5353
5454dal = SupabaseDal ()
5555recommendation_store = RecommendationStore (dal )
5656owner_store = OwnerStore ()
5757
58+
5859class AdmissionReview (BaseModel ):
5960 apiVersion : str
6061 kind : str
6162 request : Dict [str , Any ]
6263
64+
6365def admission_allowed (request : AdmissionReview ) -> Dict [str , Any ]:
64- return \
65- {
66- "apiVersion" : "admission.k8s.io/v1" ,
67- "kind" : "AdmissionReview" ,
68- "response" : {
69- "uid" : request .request .get ('uid' ),
70- "allowed" : True
71- }
66+ return {
67+ "apiVersion" : "admission.k8s.io/v1" ,
68+ "kind" : "AdmissionReview" ,
69+ "response" : {"uid" : request .request .get ("uid" ), "allowed" : True },
7270 }
7371
72+
7473def enforce_pod (pod : Dict [str , Any ]) -> bool :
75- mode = pod .get (' metadata' , {}).get (' annotations' , {}).get ("admission.robusta.dev/krr-mutation-mode" , None )
74+ mode = pod .get (" metadata" , {}).get (" annotations" , {}).get ("admission.robusta.dev/krr-mutation-mode" , None )
7675 if mode == ENFORCE :
7776 return True
7877 elif mode == IGNORE :
@@ -85,30 +84,29 @@ def enforce_pod(pod: Dict[str, Any]) -> bool:
8584async def mutate (request : AdmissionReview ):
8685 """
8786 Handle mutating webhook requests from Kubernetes.
88-
87+
8988 Args:
9089 request (AdmissionReview): The admission review request from Kubernetes
91-
90+
9291 Returns:
9392 dict: Admission review response
9493 """
9594 start_time = time .time ()
9695 try :
9796 logging .debug ("Admission request received %s" , request )
9897 # Extract the object being reviewed
99- object_to_review = request .request .get (' object' , {})
100- kind = request .request .get (' kind' , {}).get (' kind' )
98+ object_to_review = request .request .get (" object" , {})
99+ kind = request .request .get (" kind" , {}).get (" kind" )
101100
102101 if kind == "ReplicaSet" : # use create/delete admission requests, to track new/removed replica sets owners
103102 owner_store .handle_rs_admission (request .request )
104- operation = request .request .get (' operation' , ' UNKNOWN' )
103+ operation = request .request .get (" operation" , " UNKNOWN" )
105104 replicaset_admissions .labels (operation = operation ).inc ()
106- admission_duration .labels (kind = ' ReplicaSet' ).observe (time .time () - start_time )
105+ admission_duration .labels (kind = " ReplicaSet" ).observe (time .time () - start_time )
107106 # Update rs_owners size metric
108107 rs_owners_size .set (owner_store .get_rs_owners_count ())
109108 return admission_allowed (request )
110109
111-
112110 if kind != "Pod" :
113111 logger .warning (f"Received unexpected resource mutation: { kind } " )
114112 return admission_allowed (request )
@@ -144,12 +142,12 @@ async def mutate(request: AdmissionReview):
144142 logger .debug ("Pod Recommendations %s" , recommendations )
145143
146144 patches = []
147-
145+
148146 containers = object_to_review .get ("spec" , {}).get ("containers" , [])
149147 for i , container in enumerate (containers ):
150148 container_name = container .get ("name" )
151149 patches .extend (patch_container_resources (i , container , recommendations .get (container_name )))
152-
150+
153151 # Record metrics for Pod mutation
154152 was_mutated = len (patches ) > 0
155153 reason = "success" if was_mutated else "no_changes_needed"
@@ -166,91 +164,96 @@ async def mutate(request: AdmissionReview):
166164 response ["patchType" ] = "JSONPatch"
167165 response ["patch" ] = base64 .b64encode (json .dumps (patches ).encode ()).decode ()
168166
169- return {
170- "apiVersion" : "admission.k8s.io/v1" ,
171- "kind" : "AdmissionReview" ,
172- "response" : response
173- }
174-
167+ return {"apiVersion" : "admission.k8s.io/v1" , "kind" : "AdmissionReview" , "response" : response }
168+
175169 except Exception as e :
176170 logger .exception ("Error processing webhook request" )
177171 # Record failure metric for Pod requests
178- if request .request .get (' kind' , {}).get (' kind' ) == "Pod" :
172+ if request .request .get (" kind" , {}).get (" kind" ) == "Pod" :
179173 pod_admission_mutations .labels (mutated = "false" , reason = "processing_error" ).inc ()
180174 admission_duration .labels (kind = "Pod" ).observe (time .time () - start_time )
181175 raise HTTPException (status_code = 500 , detail = str (e ))
182176
177+
183178@app .get ("/health" )
184179async def health_check ():
185180 """
186181 Health check endpoint.
187-
182+
188183 Returns:
189184 dict: Health status
190185 """
191186 owner_store .finalize_owner_initialization () # Init loading owners from api server, after accepting api requests
192187 return {"status" : "healthy" }
193188
189+
194190@app .get ("/recommendations/{namespace}/{kind}/{name}" )
195191async def get_recommendations (namespace : str , kind : str , name : str ):
196192 """
197193 Get recommendations for a workload.
198-
194+
199195 Args:
200196 namespace: Kubernetes namespace
201197 kind: Workload kind (e.g., Deployment, StatefulSet)
202198 name: Workload name
203-
199+
204200 Returns:
205201 dict: Recommendations per container or 404 if not found
206202 """
207203 try :
208204 recommendations : WorkloadRecommendation = recommendation_store .get_recommendations (
209205 name = name , namespace = namespace , kind = kind
210206 )
211-
207+
212208 if not recommendations :
213209 raise HTTPException (status_code = 404 , detail = "No recommendations found for this workload" )
214-
210+
215211 result = {}
216212 for container_name , container_recommendation in recommendations .container_recommendations .items ():
217213 result [container_name ] = {
218- "cpu" : {
219- "request" : container_recommendation .cpu .request ,
220- "limit" : container_recommendation .cpu .limit
221- } if container_recommendation .cpu else None ,
222- "memory" : {
223- "request" : container_recommendation .memory .request ,
224- "limit" : container_recommendation .memory .limit
225- } if container_recommendation .memory else None
214+ "cpu" : (
215+ {"request" : container_recommendation .cpu .request , "limit" : container_recommendation .cpu .limit }
216+ if container_recommendation .cpu
217+ else None
218+ ),
219+ "memory" : (
220+ {"request" : container_recommendation .memory .request , "limit" : container_recommendation .memory .limit }
221+ if container_recommendation .memory
222+ else None
223+ ),
226224 }
227-
228- return {
229- "namespace" : namespace ,
230- "kind" : kind ,
231- "name" : name ,
232- "containers" : result
233- }
234-
225+
226+ return {"namespace" : namespace , "kind" : kind , "name" : name , "containers" : result }
227+
235228 except HTTPException :
236229 raise
237230 except Exception as e :
238231 logger .exception ("Error retrieving recommendations" )
239232 raise HTTPException (status_code = 500 , detail = str (e ))
240233
234+
241235@app .get ("/metrics" )
242236async def metrics ():
243237 """
244238 Prometheus metrics endpoint.
245-
239+
246240 Returns:
247241 Response: Prometheus metrics in text format
248242 """
249243 # Update rs_owners size metric before returning metrics
250244 rs_owners_size .set (owner_store .get_rs_owners_count ())
251245 return Response (generate_latest (), media_type = CONTENT_TYPE_LATEST )
252246
247+
253248if __name__ == "__main__" :
254249 import uvicorn
250+
255251 logger .info ("Starting Kubernetes Webhook server on 8443..." )
256- uvicorn .run (app , host = "0.0.0.0" , port = 8443 , ssl_keyfile = ENFORCER_SSL_KEY_FILE , ssl_certfile = ENFORCER_SSL_CERT_FILE , log_level = "warning" )
252+ uvicorn .run (
253+ app ,
254+ host = "0.0.0.0" ,
255+ port = 8443 ,
256+ ssl_keyfile = ENFORCER_SSL_KEY_FILE ,
257+ ssl_certfile = ENFORCER_SSL_CERT_FILE ,
258+ log_level = "warning" ,
259+ )
0 commit comments