@@ -63,20 +63,26 @@ def fetch_cluster_credentials(
6363 raise
6464
6565
66- def deploy_gke_yaml (yaml : str ) -> None :
66+ def deploy_gke_yaml (yaml : str , action : str = "apply" ) -> None :
6767 """Deploys the given YAML to the GKE cluster.
6868
6969 Args:
7070 yaml: The GKE YAML to deploy.
71+ action: The kubectl action to perform ("apply" or "create"). Create is
72+ equivalent to "apply" but does not support "replacing" the resource if it
73+ already exists.
7174
7275 Raises:
7376 subprocess.CalledProcessError: If the kubectl command fails.
77+ ValueError: If action is not "apply" or "create".
7478 """
75- _logger .info ("Deploying GKE YAML: %s" , yaml )
76- kubectl_apply_command = ["kubectl" , "apply" , "-f" , "-" ]
79+ if action not in ("apply" , "create" ):
80+ raise ValueError (f"Invalid kubectl action: { action } " )
81+ _logger .info ("Deploying GKE YAML with action %s: %s" , action , yaml )
82+ kubectl_command = ["kubectl" , action , "-f" , "-" ]
7783 try :
7884 proxy_result = subprocess .run (
79- kubectl_apply_command ,
85+ kubectl_command ,
8086 input = yaml ,
8187 check = True ,
8288 capture_output = True ,
@@ -93,6 +99,49 @@ def deploy_gke_yaml(yaml: str) -> None:
9399 )
94100
95101
102+ def delete_gke_resource (
103+ resource_type : str , name : str , namespace : str = "default"
104+ ) -> None :
105+ """Deletes the given resource from the GKE cluster.
106+
107+ Args:
108+ resource_type: The type of resource to delete (e.g. "deployment",
109+ "service", "job").
110+ name: The name of the resource.
111+ namespace: The namespace of the resource.
112+ """
113+ _validate_k8s_name (resource_type )
114+ _validate_k8s_name (name )
115+ _validate_k8s_name (namespace )
116+ _logger .info (
117+ "Deleting %s: %s in namespace: %s" , resource_type , name , namespace
118+ )
119+ command = [
120+ "kubectl" ,
121+ "delete" ,
122+ resource_type ,
123+ "-n" ,
124+ namespace ,
125+ "--ignore-not-found" ,
126+ "--" ,
127+ name ,
128+ ]
129+ try :
130+ result = subprocess .run (
131+ command ,
132+ check = True ,
133+ capture_output = True ,
134+ text = True ,
135+ )
136+ _logger .info ("Successfully deleted %s. %s" , resource_type , result .stdout )
137+ except subprocess .CalledProcessError as e :
138+ _logger .exception (
139+ "Failed to delete %s. kubectl output:\n %r" , resource_type , e .stderr
140+ )
141+ raise
142+
143+
144+
96145def get_pod_from_job (job_name : str ) -> str :
97146 """Returns the pod name for the given job.
98147
@@ -224,7 +273,7 @@ def wait_for_pod(job_name: str) -> str:
224273 return check_pod_ready (pod_name )
225274
226275
227- def __test_pod_connection (port : int ) -> None :
276+ def _test_remote_connection (port : int ) -> None :
228277 """Tests the connection to the pod.
229278
230279 Args:
@@ -233,20 +282,22 @@ def __test_pod_connection(port: int) -> None:
233282 _logger .info ("Connecting to localhost:%d" , port )
234283 try :
235284 with socket .create_connection (("localhost" , port ), timeout = 30 ):
236- _logger .info ("Pod is ready." )
285+ _logger .info ("Connection to localhost:%d is ready." , port )
237286 except (socket .timeout , ConnectionRefusedError ) as exc :
238287 raise RuntimeError ("Could not connect to the pod." ) from exc
239288
240289
241290def enable_port_forwarding (
242- pod_name : str ,
291+ remote_server : str ,
243292 server_port : int ,
293+ namespace : str = "default" ,
244294) -> tuple [int , subprocess .Popen [str ]]:
245295 """Enables port forwarding for the given pod.
246296
247297 Args:
248- pod_name : The name of the pod.
298+ remote_server : The name of the pod or service .
249299 server_port: The port of the server to forward to.
300+ namespace: The namespace of the pod.
250301
251302 Returns:
252303 A tuple containing the pod port and the port forwarding process.
@@ -255,28 +306,36 @@ def enable_port_forwarding(
255306 cannot be established.
256307 """
257308 try :
258- port_available = portpicker .pick_unused_port ()
309+ local_port = portpicker .pick_unused_port ()
259310 except Exception as e :
260311 _logger .exception ("Error finding free local port: %r" , e )
261312 raise
262313
263- _logger .info ("Found free local port: %d" , port_available )
314+ _logger .info ("Found free local port: %d" , local_port )
264315 _logger .info (
265316 "Starting port forwarding from local port %d to %s:%d" ,
266- port_available ,
267- pod_name ,
317+ local_port ,
318+ remote_server ,
268319 server_port ,
269320 )
270321
271- _validate_k8s_name (pod_name )
322+ if "/" in remote_server :
323+ parts = remote_server .split ("/" , 1 )
324+ _validate_k8s_name (parts [0 ])
325+ _validate_k8s_name (parts [1 ])
326+ else :
327+ _validate_k8s_name (remote_server )
328+ _validate_k8s_name (namespace )
272329 port_forward_command = [
273330 "kubectl" ,
274331 "port-forward" ,
332+ "-n" ,
333+ namespace ,
275334 "--address" ,
276335 "localhost" ,
277336 "--" ,
278- f"pod/ { pod_name } " ,
279- f"{ port_available } :{ server_port } " ,
337+ f"{ remote_server } " ,
338+ f"{ local_port } :{ server_port } " ,
280339 ]
281340 try :
282341 # Start port forwarding in the background.
@@ -316,12 +375,12 @@ def enable_port_forwarding(
316375 )
317376
318377 try :
319- __test_pod_connection ( port_available )
378+ _test_remote_connection ( local_port )
320379 except Exception :
321380 port_forward_process .terminate ()
322381 raise
323382
324- return (port_available , port_forward_process )
383+ return (local_port , port_forward_process )
325384
326385
327386def stream_pod_logs (pod_name : str ) -> subprocess .Popen [str ]:
@@ -351,30 +410,68 @@ def stream_pod_logs(pod_name: str) -> subprocess.Popen[str]:
351410 raise
352411
353412
354- def delete_gke_job (job_name : str ) -> None :
355- """Deletes the given job from the GKE cluster.
356413
357- Args:
358- job_name: The name of the job.
359- """
360- _validate_k8s_name (job_name )
361- _logger .info ("Deleting job: %s" , job_name )
362- delete_job_command = [
414+ def wait_for_deployment (
415+ name : str , namespace : str = "default" , timeout : int = 300
416+ ) -> None :
417+ """Waits for deployment to be ready."""
418+ _validate_k8s_name (name )
419+ _validate_k8s_name (namespace )
420+ _logger .info ("Waiting for deployment %s to be ready..." , name )
421+ command = [
363422 "kubectl" ,
364- "delete" ,
365- "job" ,
366- "--ignore-not-found" ,
367- "--" ,
368- job_name ,
423+ "rollout" ,
424+ "status" ,
425+ f"deployment/{ name } " ,
426+ "-n" ,
427+ namespace ,
428+ f"--timeout={ timeout } s" ,
369429 ]
370430 try :
371- result = subprocess .run (
372- delete_job_command ,
373- check = True ,
374- capture_output = True ,
375- text = True ,
376- )
431+ subprocess .run (command , check = True , capture_output = True , text = True )
377432 except subprocess .CalledProcessError as e :
378- _logger .exception ("Failed to delete job. kubectl output:\\ n%r" , e .stderr )
379- raise
380- _logger .info ("Successfully deleted job. %s" , result .stdout )
433+ _logger .exception ("Deployment failed to become ready: %r" , e )
434+ raise RuntimeError (f"Deployment did not become ready: { e .stderr } " ) from e
435+ _logger .info ("Deployment %s is ready." , name )
436+
437+
438+ def wait_for_service_ip (
439+ name : str , namespace : str = "default" , timeout : int = 300
440+ ) -> str :
441+ """Waits for service to get an external IP and returns it."""
442+ _validate_k8s_name (name )
443+ start_time = time .time ()
444+ while time .time () - start_time < timeout :
445+ command = [
446+ "kubectl" ,
447+ "get" ,
448+ "svc" ,
449+ name ,
450+ "-n" ,
451+ namespace ,
452+ "-o" ,
453+ "jsonpath={.status.loadBalancer.ingress[0].ip}" ,
454+ ]
455+ try :
456+ result = subprocess .run (
457+ command , check = True , capture_output = True , text = True
458+ )
459+ ip = result .stdout .strip ()
460+ if ip :
461+ _logger .info ("Service IP assigned: %s" , ip )
462+ return ip
463+ except subprocess .CalledProcessError as e :
464+ _logger .warning ("Failed to get service IP: %r" , e )
465+ time .sleep (2 )
466+ raise RuntimeError (f"Timeout waiting for service IP for { name } " )
467+
468+
469+ def pick_unused_local_port () -> int :
470+ """Picks an unused local port."""
471+ return portpicker .pick_unused_port ()
472+
473+
474+ def is_local_port_free (port : int ) -> bool :
475+ """Checks if a local port is free."""
476+ return portpicker .is_port_free (port )
477+
0 commit comments