Skip to content

Commit 2c7efed

Browse files
guptaakacopybara-github
authored andcommitted
Add a script to deploy VS Code on GKE CPU node pool
PiperOrigin-RevId: 930204177
1 parent 35407e8 commit 2c7efed

4 files changed

Lines changed: 358 additions & 41 deletions

File tree

pathwaysutils/experimental/shared_pathways_service/gke_utils.py

Lines changed: 136 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
96145
def 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

241290
def 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

327386
def 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+

pathwaysutils/experimental/shared_pathways_service/isc_pathways.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ def __enter__(self):
339339
self.proxy_pod_name = gke_utils.wait_for_pod(self._proxy_job_name)
340340
self._proxy_port, self._port_forward_process = (
341341
gke_utils.enable_port_forwarding(
342-
self.proxy_pod_name, PROXY_SERVER_PORT
342+
f"pod/{self.proxy_pod_name}", PROXY_SERVER_PORT
343343
)
344344
)
345345

@@ -394,7 +394,7 @@ def _cleanup(self) -> None:
394394

395395
# Delete the proxy GKE job.
396396
_logger.info("Deleting Pathways proxy...")
397-
gke_utils.delete_gke_job(self._proxy_job_name)
397+
gke_utils.delete_gke_resource("job", self._proxy_job_name)
398398
_logger.info("Pathways proxy GKE job deletion complete.")
399399

400400
# Restore JAX variables.

0 commit comments

Comments
 (0)