|
11 | 11 | # limitations under the License. |
12 | 12 | """Pathways JobSet generator and builder (with Worker Job Config).""" |
13 | 13 |
|
| 14 | +import hashlib |
14 | 15 | import json |
15 | 16 | import logging |
16 | 17 | import math |
17 | | -from typing import Any, Mapping |
| 18 | +from typing import Any, Mapping, Sequence |
18 | 19 | from kubernetes import client |
19 | 20 |
|
20 | 21 | # GKE sidecar containers restartPolicy compatibility placeholder. |
@@ -530,6 +531,90 @@ def _build_worker_job_template( |
530 | 531 | ) |
531 | 532 | return worker_job_template |
532 | 533 |
|
| 534 | + def _filter_matching_containers( |
| 535 | + self, |
| 536 | + containers_param: str | Sequence[str], |
| 537 | + all_containers: list[client.V1Container], |
| 538 | + ) -> list[client.V1Container]: |
| 539 | + """Filters containers matching the containers_param selection.""" |
| 540 | + if containers_param == "all": |
| 541 | + return all_containers |
| 542 | + if containers_param == "worker": |
| 543 | + return [c for c in all_containers if c.name == "pathways-worker"] |
| 544 | + |
| 545 | + filter_names = ( |
| 546 | + [containers_param] |
| 547 | + if isinstance(containers_param, str) |
| 548 | + else set(containers_param) |
| 549 | + ) |
| 550 | + return [c for c in all_containers if c.name in filter_names] |
| 551 | + |
| 552 | + def _enable_gcsfuse_annotations( |
| 553 | + self, job_template: client.V1JobTemplateSpec |
| 554 | + ) -> None: |
| 555 | + """Enables gke-gcsfuse/volumes annotation on job and pod metadata.""" |
| 556 | + job_metadata = job_template.metadata or client.V1ObjectMeta() |
| 557 | + job_annotations = job_metadata.annotations or {} |
| 558 | + job_annotations["gke-gcsfuse/volumes"] = "true" |
| 559 | + job_metadata.annotations = job_annotations |
| 560 | + job_template.metadata = job_metadata |
| 561 | + |
| 562 | + pod_metadata = job_template.spec.template.metadata or client.V1ObjectMeta() |
| 563 | + pod_annotations = pod_metadata.annotations or {} |
| 564 | + pod_annotations["gke-gcsfuse/volumes"] = "true" |
| 565 | + pod_metadata.annotations = pod_annotations |
| 566 | + job_template.spec.template.metadata = pod_metadata |
| 567 | + |
| 568 | + def _add_volume_to_pod_spec( |
| 569 | + self, pod_spec: client.V1PodSpec, volume: client.V1Volume |
| 570 | + ) -> None: |
| 571 | + """Appends volume to pod_spec if not already present.""" |
| 572 | + volumes = pod_spec.volumes or [] |
| 573 | + if not any(v.name == volume.name for v in volumes): |
| 574 | + volumes.append(volume) |
| 575 | + pod_spec.volumes = volumes |
| 576 | + |
| 577 | + def add_gcsfuse( |
| 578 | + self, |
| 579 | + containers: str | Sequence[str], |
| 580 | + mount_path: str, |
| 581 | + bucket: str, |
| 582 | + read_only: bool = False, |
| 583 | + ) -> "PathwaysJobSet": |
| 584 | + """Adds GCSFuse mount to specified containers.""" |
| 585 | + bucket_hash = int(hashlib.md5(bucket.encode()).hexdigest(), 16) % (10**8) |
| 586 | + volume_name = f"gcsfuse-{bucket_hash}" |
| 587 | + volume = client.V1Volume( |
| 588 | + name=volume_name, |
| 589 | + csi=client.V1CSIVolumeSource( |
| 590 | + driver="gcsfuse.csi.storage.gke.io", |
| 591 | + volume_attributes={"bucketName": bucket}, |
| 592 | + ), |
| 593 | + ) |
| 594 | + volume_mount = client.V1VolumeMount( |
| 595 | + name=volume_name, |
| 596 | + mount_path=mount_path, |
| 597 | + read_only=read_only, |
| 598 | + ) |
| 599 | + |
| 600 | + for job_template in (self._head_job_template, self._worker_job_template): |
| 601 | + pod_spec = job_template.spec.template.spec |
| 602 | + all_containers = (pod_spec.containers or []) + (pod_spec.init_containers or []) |
| 603 | + |
| 604 | + matching = self._filter_matching_containers(containers, all_containers) |
| 605 | + if not matching: |
| 606 | + continue |
| 607 | + |
| 608 | + self._enable_gcsfuse_annotations(job_template) |
| 609 | + self._add_volume_to_pod_spec(pod_spec, volume) |
| 610 | + |
| 611 | + for container in matching: |
| 612 | + volume_mounts = container.volume_mounts or [] |
| 613 | + volume_mounts.append(volume_mount) |
| 614 | + container.volume_mounts = volume_mounts |
| 615 | + |
| 616 | + return self |
| 617 | + |
533 | 618 | def _compile_config(self) -> dict[str, Any]: |
534 | 619 | """Compiles the JobSet configuration into a dictionary.""" |
535 | 620 | with client.ApiClient() as api_client: |
|
0 commit comments