diff --git a/cmd/job/submit.go b/cmd/job/submit.go index d90fd16903..051051bae2 100644 --- a/cmd/job/submit.go +++ b/cmd/job/submit.go @@ -137,7 +137,7 @@ func init() { SubmitCmd.Flags().StringVarP(&dryRunManifest, "dry-run-out", "o", "", "Path to output the generated Kubernetes manifest instead of applying it.") SubmitCmd.Flags().StringVarP(&platform, "platform", "f", "linux/amd64", "Target platform for the image build (e.g., 'linux/amd64', 'linux/arm64'). Used with --base-image.") - SubmitCmd.Flags().StringSliceVar(&volumeStr, "mount", nil, "Volumes to mount (format: :[:], mode can be 'ro' or 'rw', default 'ro').") + SubmitCmd.Flags().StringArrayVar(&volumeStr, "mount", nil, "Volumes to mount (format: ;[;][;options=], mode can be 'ro' or 'rw', default 'ro').") SubmitCmd.Flags().StringArrayVar(&envVars, "env", []string{}, "Custom environment variables to pass to the workload container in KEY=VALUE format. Can be specified multiple times.") SubmitCmd.Flags().StringVarP(&workloadName, "name", "n", "", "Name of the workload to create. Required.") diff --git a/docs/gcluster_job_guide.md b/docs/gcluster_job_guide.md index 026c300310..f6a919a5a0 100644 --- a/docs/gcluster_job_guide.md +++ b/docs/gcluster_job_guide.md @@ -161,8 +161,9 @@ If you want to run a job across multiple groups of GPU nodes (e.g., 2 groups of You can mount Cloud Storage buckets, Filestore instances, existing PVCs (e.g., for Lustre), or host paths using the `--mount` flag. -Mounts must use the format: `--mount ":[:]"` -* `mode` is optional and defaults to `ro` (read-only). To allow writes, append `:rw`. +Mounts must use the format: `--mount ";[;][;options=]"` +* `mode` is optional and defaults to `ro` (read-only). To allow writes, append `;rw`. +* `options` is optional and allows passing custom mount options (currently only supported for GCS fuse volumes). **Supported volume sources (``):** * **Cloud Storage**: `gs://` (mounts via GCS Fused Driver) @@ -187,7 +188,7 @@ Mounting a GCS bucket (read-write): --compute-type n2-standard-32 \ --base-image python:3.9-slim \ --build-context job_details \ - --mount "gs://:/data:rw" + --mount "gs://;/data;rw;options=logging:severity:info,enable-atomic-rename-object:true" ``` Mounting an existing PVC named `lustre-pvc` (read-only): @@ -199,7 +200,7 @@ Mounting an existing PVC named `lustre-pvc` (read-only): --compute-type n2-standard-32 \ --base-image python:3.9-slim \ --build-context job_details \ - --mount "lustre-pvc:/data" + --mount "lustre-pvc;/data" ``` ### 4.5 Example: Submit Job with Custom Environment Variables @@ -1094,7 +1095,7 @@ The `gcluster job submit` command deploys a container image as a job (Kubernetes | `--num-slices` | `int` | Number of independent groups/slices to use (Default: `1`). | | `--num-nodes` | `int` | Number of nodes to use per group/slice (Default: `1`). Auto-calculated for TPUs based on topology. | | `--restarts` | `int` | Maximum number of restarts allowed for the JobSet before marked as failed (Default: `1`). | -| `--mount` | `stringArray` | Mount storage volumes, buckets, filestore instances, or PVCs using the `:[:]` format. Examples of ``: `gs://my-bucket`, `filestore://my-instance/share`, `my-pvc` (for Lustre/etc), or `/host/path`. | +| `--mount` | `stringArray` | Mount storage volumes, buckets, filestore instances, or PVCs using the `;[;][;options=]` format. Examples of ``: `gs://my-bucket`, `filestore://my-instance/share`, `my-pvc` (for Lustre/etc), or `/host/path`. | | `--env` | `stringArray` | Custom environment variables to pass exclusively to the user's workload container in KEY=VALUE format (e.g. `--env KEY=VALUE`). Applies to both standard and Pathways workloads. Can be specified multiple times. | | `--await-job-completion` | `bool` | If true, the CLI waits for the job to complete before exiting. | | `--timeout` | `string` | Time to wait for job completion (e.g., `1h`, `10m`). Used with `--await-job-completion`. | diff --git a/pkg/orchestrator/gke/gke_job_orchestrator_test.go b/pkg/orchestrator/gke/gke_job_orchestrator_test.go index b8d0996606..0e943d8561 100644 --- a/pkg/orchestrator/gke/gke_job_orchestrator_test.go +++ b/pkg/orchestrator/gke/gke_job_orchestrator_test.go @@ -310,9 +310,9 @@ func TestGenerateGKEManifest_Volumes(t *testing.T) { ClusterLocation: "us-central1-a", ComputeType: "n2-standard-4", RawMounts: []string{ - "gs://my-bucket:/data", - "/host/path:/host", - "my-pvc:/pvc", + "gs://my-bucket;/data", + "/host/path;/host", + "my-pvc;/pvc", }, } diff --git a/pkg/orchestrator/gke/storage.go b/pkg/orchestrator/gke/storage.go index 4e78506ec0..9ec831edec 100644 --- a/pkg/orchestrator/gke/storage.go +++ b/pkg/orchestrator/gke/storage.go @@ -41,7 +41,7 @@ func (sm *StorageManager) ProcessMounts(mounts []string, job orchestrator.JobDef var mountInfos []MountInfo var additionalManifests []string for i, vStr := range mounts { - src, dest, readOnly, err := sm.parseSingleVolume(vStr) + src, dest, options, readOnly, err := sm.parseSingleVolume(vStr) if err != nil { return nil, nil, err } @@ -65,13 +65,15 @@ func (sm *StorageManager) ProcessMounts(mounts []string, job orchestrator.JobDef volType = "hostPath" } - mountInfos = append(mountInfos, MountInfo{ + mountInfo := MountInfo{ Name: fmt.Sprintf("vol-%d", i), Source: src, MountPath: dest, Type: volType, ReadOnly: readOnly, - }) + Options: options, + } + mountInfos = append(mountInfos, mountInfo) } return mountInfos, additionalManifests, nil @@ -83,7 +85,7 @@ func (sm *StorageManager) ValidateMounts(mounts []string) error { seenDestinations := make(map[string]bool) for _, vStr := range mounts { - src, dest, _, err := sm.parseSingleVolume(vStr) + src, dest, _, _, err := sm.parseSingleVolume(vStr) if err != nil { return err } @@ -97,68 +99,51 @@ func (sm *StorageManager) ValidateMounts(mounts []string) error { seenSources[src] = true seenDestinations[dest] = true } - return nil } -func (sm *StorageManager) parseSingleVolume(vStr string) (src, dest string, readOnly bool, err error) { - src, dest, readOnly, err = parseSrcDest(vStr) - if err != nil { - return "", "", false, err - } - if err := validateSrcScheme(src, vStr); err != nil { - return "", "", false, err - } - return src, dest, readOnly, nil -} - func missingDestOrFormatErr(vStr string) error { if strings.HasPrefix(vStr, "gs://") || strings.HasPrefix(vStr, "filestore://") { return fmt.Errorf("invalid volume format: %s. Missing destination.", vStr) } - return fmt.Errorf("invalid volume format: %s. Expected format: :[:]", vStr) -} - -func parseVolumeMode(vStr string) (stripped string, readOnly bool, err error) { - idx := strings.LastIndex(vStr, ":") - if idx <= 0 || idx == len(vStr)-1 { - return "", false, missingDestOrFormatErr(vStr) - } - - lastPart := vStr[idx+1:] - if lastPart == "ro" || lastPart == "rw" { - return vStr[:idx], (lastPart == "ro"), nil - } - return vStr, true, nil + return fmt.Errorf("invalid volume format: %s. Expected format: ;[;][;options=]", vStr) } -func parseSrcDest(vStr string) (src, dest string, readOnly bool, err error) { - origVStr := vStr - - vStrWithoutMode, readOnly, err := parseVolumeMode(vStr) - if err != nil { - return "", "", false, err +func (sm *StorageManager) parseSingleVolume(vStr string) (src, dest, options string, readOnly bool, err error) { + parts := strings.Split(vStr, ";") + if len(parts) < 2 { + return "", "", "", false, missingDestOrFormatErr(vStr) + } + + src = parts[0] + dest = parts[1] + readOnly = true // default + + for _, part := range parts[2:] { + if part == "ro" { + readOnly = true + } else if part == "rw" { + readOnly = false + } else if strings.HasPrefix(part, "options=") { + options = strings.TrimPrefix(part, "options=") + } else { + return "", "", "", false, fmt.Errorf("invalid volume option or mode: %s", part) + } } - idx := strings.LastIndex(vStrWithoutMode, ":") - if idx <= 0 || idx == len(vStrWithoutMode)-1 { - return "", "", false, missingDestOrFormatErr(origVStr) + if src == "" || dest == "" || !strings.HasPrefix(dest, "/") { + return "", "", "", false, missingDestOrFormatErr(vStr) } - src = vStrWithoutMode[:idx] - dest = vStrWithoutMode[idx+1:] - - if src == "" || dest == "" || !strings.HasPrefix(dest, "/") { - return "", "", false, missingDestOrFormatErr(origVStr) + if options != "" && !strings.HasPrefix(src, "gs://") { + return "", "", "", false, fmt.Errorf("options= is currently only supported for GCS fuse volumes (gs://...)") } - // Ensure scheme colons are not mistaken for delimiters when destination is missing - if (strings.HasPrefix(vStrWithoutMode, "gs://") && !strings.HasPrefix(src, "gs://")) || - (strings.HasPrefix(vStrWithoutMode, "filestore://") && !strings.HasPrefix(src, "filestore://")) { - return "", "", false, missingDestOrFormatErr(origVStr) + if err := validateSrcScheme(src, vStr); err != nil { + return "", "", "", false, err } - return src, dest, readOnly, nil + return src, dest, options, readOnly, nil } func extractHost(hostStr string) string { @@ -332,12 +317,16 @@ func buildVolumeSpec(v MountInfo) map[string]interface{} { } switch v.Type { case "gcsfuse": + volumeAttributes := map[string]interface{}{ + "bucketName": strings.TrimPrefix(v.Source, "gs://"), + } + if v.Options != "" { + volumeAttributes["mountOptions"] = v.Options + } spec["csi"] = map[string]interface{}{ - "driver": "gcsfuse.csi.storage.gke.io", - "readOnly": v.ReadOnly, - "volumeAttributes": map[string]interface{}{ - "bucketName": strings.TrimPrefix(v.Source, "gs://"), - }, + "driver": "gcsfuse.csi.storage.gke.io", + "readOnly": v.ReadOnly, + "volumeAttributes": volumeAttributes, } case "hostPath": spec["hostPath"] = map[string]interface{}{ diff --git a/pkg/orchestrator/gke/storage_test.go b/pkg/orchestrator/gke/storage_test.go index 904a7608f2..6047bcc0e4 100644 --- a/pkg/orchestrator/gke/storage_test.go +++ b/pkg/orchestrator/gke/storage_test.go @@ -34,12 +34,13 @@ func TestParseSingleVolume(t *testing.T) { wantSrc string wantDest string wantRO bool + wantOpts string wantErr bool wantErrSub string }{ { name: "valid hostPath", - input: "/host/path:/container/path", + input: "/host/path;/container/path", wantSrc: "/host/path", wantDest: "/container/path", wantRO: true, @@ -47,7 +48,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid hostPath rw", - input: "/host/path:/container/path:rw", + input: "/host/path;/container/path;rw", wantSrc: "/host/path", wantDest: "/container/path", wantRO: false, @@ -55,7 +56,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid pvc", - input: "my-pvc:/data", + input: "my-pvc;/data", wantSrc: "my-pvc", wantDest: "/data", wantRO: true, @@ -63,7 +64,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid gcsfuse", - input: "gs://my-bucket:/data", + input: "gs://my-bucket;/data", wantSrc: "gs://my-bucket", wantDest: "/data", wantRO: true, @@ -71,7 +72,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid filestore", - input: "filestore://my-instance/share:/data", + input: "filestore://my-instance/share;/data", wantSrc: "filestore://my-instance/share", wantDest: "/data", wantRO: true, @@ -79,7 +80,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid filestore with port", - input: "filestore://10.0.0.2:2049/share:/data", + input: "filestore://10.0.0.2:2049/share;/data", wantSrc: "filestore://10.0.0.2:2049/share", wantDest: "/data", wantRO: true, @@ -87,7 +88,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid filestore with port and mode", - input: "filestore://10.0.0.2:2049/share:/data:rw", + input: "filestore://10.0.0.2:2049/share;/data;rw", wantSrc: "filestore://10.0.0.2:2049/share", wantDest: "/data", wantRO: false, @@ -95,13 +96,13 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "invalid scheme", - input: "parallelstore://my-instance/share:/data", + input: "parallelstore://my-instance/share;/data", wantErr: true, wantErrSub: "Unsupported scheme", }, { name: "invalid scheme with mode", - input: "parallelstore://my-instance/share:/data:ro", + input: "parallelstore://my-instance/share;/data;ro", wantErr: true, wantErrSub: "Unsupported scheme", }, @@ -119,13 +120,13 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "missing destination for gcsfuse with mode", - input: "gs://my-bucket:rw", + input: "gs://my-bucket;rw", wantErr: true, wantErrSub: "Missing destination", }, { name: "missing destination for filestore with mode", - input: "filestore://my-instance/share:ro", + input: "filestore://my-instance/share;ro", wantErr: true, wantErrSub: "Missing destination", }, @@ -137,7 +138,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid filestore IPv6", - input: "filestore://[2001:db8::1]/share:/data", + input: "filestore://[2001:db8::1]/share;/data", wantSrc: "filestore://[2001:db8::1]/share", wantDest: "/data", wantRO: true, @@ -145,7 +146,7 @@ func TestParseSingleVolume(t *testing.T) { }, { name: "valid filestore IPv6 rw", - input: "filestore://[2001:db8::1]/share:/data:rw", + input: "filestore://[2001:db8::1]/share;/data;rw", wantSrc: "filestore://[2001:db8::1]/share", wantDest: "/data", wantRO: false, @@ -157,11 +158,49 @@ func TestParseSingleVolume(t *testing.T) { wantErr: true, wantErrSub: "Missing destination", }, + { + name: "valid gcsfuse with options", + input: "gs://my-bucket;/data;options=logging:severity:info", + wantSrc: "gs://my-bucket", + wantDest: "/data", + wantRO: true, + wantOpts: "logging:severity:info", + }, + { + name: "valid gcsfuse with options and rw", + input: "gs://my-bucket;/data;rw;options=logging:severity:info", + wantSrc: "gs://my-bucket", + wantDest: "/data", + wantRO: false, + wantOpts: "logging:severity:info", + }, + { + name: "valid gcsfuse with options and rw flipped", + input: "gs://my-bucket;/data;options=logging:severity:info;rw", + wantSrc: "gs://my-bucket", + wantDest: "/data", + wantRO: false, + wantOpts: "logging:severity:info", + }, + { + name: "options not supported for non-gcs", + input: "my-pvc;/data;options=abc", + wantErr: true, + wantErrSub: "options= is currently only supported for GCS", + }, + { + name: "valid gcsfuse with multiple comma-separated options", + input: "gs://my-bucket;/data;options=logging:severity:info,enable-atomic-rename-object:true", + wantSrc: "gs://my-bucket", + wantDest: "/data", + wantRO: true, + wantOpts: "logging:severity:info,enable-atomic-rename-object:true", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - src, dest, readOnly, err := sm.parseSingleVolume(tc.input) + src, dest, options, readOnly, err := sm.parseSingleVolume(tc.input) if (err != nil) != tc.wantErr { t.Fatalf("parseSingleVolume() error = %v, wantErr %v", err, tc.wantErr) @@ -182,6 +221,9 @@ func TestParseSingleVolume(t *testing.T) { if readOnly != tc.wantRO { t.Errorf("parseSingleVolume() readOnly = %v, want %v", readOnly, tc.wantRO) } + if options != tc.wantOpts { + t.Errorf("parseSingleVolume() options = %v, want %v", options, tc.wantOpts) + } }) } } @@ -190,8 +232,8 @@ func TestValidateMounts(t *testing.T) { sm := &StorageManager{} mounts := []string{ - "gs://my-bucket:/data", - "my-pvc:/data", // duplicate dest + "gs://my-bucket;/data", + "my-pvc;/data", // duplicate dest } err := sm.ValidateMounts(mounts) if err == nil || !strings.Contains(err.Error(), "duplicate volume destination") { @@ -199,8 +241,8 @@ func TestValidateMounts(t *testing.T) { } mounts = []string{ - "gs://my-bucket:/data1", - "gs://my-bucket:/data2", // duplicate src + "gs://my-bucket;/data1", + "gs://my-bucket;/data2", // duplicate src } err = sm.ValidateMounts(mounts) if err == nil || !strings.Contains(err.Error(), "duplicate volume source") { @@ -208,7 +250,7 @@ func TestValidateMounts(t *testing.T) { } mounts = []string{ - "parallelstore://foo:/data", // unsupported scheme + "parallelstore://foo;/data", // unsupported scheme } err = sm.ValidateMounts(mounts) if err == nil || !strings.Contains(err.Error(), "Unsupported scheme") { @@ -216,8 +258,8 @@ func TestValidateMounts(t *testing.T) { } mounts = []string{ - "gs://my-bucket:/data1", - "my-pvc:/data2", + "gs://my-bucket;/data1", + "my-pvc;/data2", } err = sm.ValidateMounts(mounts) if err != nil { @@ -230,9 +272,9 @@ func TestProcessMounts_Basic(t *testing.T) { job := orchestrator.JobDefinition{} mounts := []string{ - "gs://my-bucket:/data", - "/host/path:/host", - "my-pvc:/pvc", + "gs://my-bucket;/data", + "/host/path;/host", + "my-pvc;/pvc", } infos, manifests, err := sm.ProcessMounts(mounts, job) @@ -269,7 +311,7 @@ func TestProcessMounts_Filestore_IP(t *testing.T) { // Test valid IP-based Filestore mount mounts := []string{ - "filestore://10.0.0.2/share:/data", + "filestore://10.0.0.2/share;/data", } infos, manifests, err := sm.ProcessMounts(mounts, job) @@ -304,7 +346,7 @@ func TestProcessMounts_Filestore_Sanitize(t *testing.T) { // Test sanitization of PVC Name (lowercase, underscores and slashes to hyphens) mounts := []string{ - "filestore://10.0.0.2/MY_complex_SHARE/sub_folder/sub_subfolder:/data", + "filestore://10.0.0.2/MY_complex_SHARE/sub_folder/sub_subfolder;/data", } infos, manifests, err := sm.ProcessMounts(mounts, job) @@ -324,7 +366,7 @@ func TestProcessMounts_Filestore_Sanitize(t *testing.T) { // Test double/leading slashes in share name mounts = []string{ - "filestore://10.0.0.2//my_share_name:/data", + "filestore://10.0.0.2//my_share_name;/data", } infos, manifests, err = sm.ProcessMounts(mounts, job) if err != nil { @@ -340,7 +382,7 @@ func TestProcessMounts_Filestore_Sanitize(t *testing.T) { // Test special characters in share name (collapse multiple hyphens, trim trailing hyphens) mounts = []string{ - "filestore://10.0.0.2/share@name.with-dots_and_stuff!:/data", + "filestore://10.0.0.2/share@name.with-dots_and_stuff!;/data", } infos, manifests, err = sm.ProcessMounts(mounts, job) if err != nil { @@ -371,7 +413,7 @@ func TestProcessMounts_Filestore_Mock(t *testing.T) { ClusterLocation: "us-central1-a", } mountsMock := []string{ - "filestore://my-filestore-instance/my_share:/data", + "filestore://my-filestore-instance/my_share;/data", } infosMock, manifestsMock, errMock := smMock.ProcessMounts(mountsMock, jobMock) @@ -400,8 +442,8 @@ func TestProcessMounts_Filestore_TrailingSlash(t *testing.T) { // Test trailing slash handling in Filestore mount URI (single and multiple) mountsSlash := []string{ - "filestore://10.0.0.2/share/:/data", - "filestore://10.0.0.2/share///:/data2", + "filestore://10.0.0.2/share/;/data", + "filestore://10.0.0.2/share///;/data2", } infosSlash, manifestsSlash, errSlash := sm.ProcessMounts(mountsSlash, job) if errSlash != nil { @@ -455,19 +497,19 @@ func TestProcessMounts_Filestore_Invalid(t *testing.T) { job := orchestrator.JobDefinition{} // Test invalid empty share name - _, _, err := sm.ProcessMounts([]string{"filestore://10.0.0.2/:/data"}, job) + _, _, err := sm.ProcessMounts([]string{"filestore://10.0.0.2/;/data"}, job) if err == nil || !strings.Contains(err.Error(), "Expected format: filestore://") { t.Errorf("expected error for empty share name, got %v", err) } // Test invalid empty share name with multiple slashes - _, _, err = sm.ProcessMounts([]string{"filestore://10.0.0.2///:/data"}, job) + _, _, err = sm.ProcessMounts([]string{"filestore://10.0.0.2///;/data"}, job) if err == nil || !strings.Contains(err.Error(), "Expected format: filestore://") { t.Errorf("expected error for empty share name with multiple slashes, got %v", err) } // Test invalid empty instance name - _, _, err = sm.ProcessMounts([]string{"filestore:///share:/data"}, job) + _, _, err = sm.ProcessMounts([]string{"filestore:///share;/data"}, job) if err == nil || !strings.Contains(err.Error(), "Expected format: filestore://") { t.Errorf("expected error for empty instance name, got %v", err) } @@ -478,7 +520,7 @@ func TestProcessMounts_Filestore_Invalid(t *testing.T) { return "", "", 0, fmt.Errorf("filestore API lookup failed") }, } - _, _, err = smMockError.ProcessMounts([]string{"filestore://my-instance/share:/data"}, job) + _, _, err = smMockError.ProcessMounts([]string{"filestore://my-instance/share;/data"}, job) if err == nil || !strings.Contains(err.Error(), "filestore API lookup failed") { t.Errorf("expected API lookup error, got %v", err) } @@ -610,7 +652,7 @@ func TestProcessMounts_Filestore_Ambiguous(t *testing.T) { ClusterLocation: "us-central1-a", } mounts1 := []string{ - "filestore://my-filestore/share:/data", + "filestore://my-filestore/share;/data", } infos1, manifests1, err := sm.ProcessMounts(mounts1, job1) @@ -685,7 +727,7 @@ func TestProcessMounts_Filestore_Fallback(t *testing.T) { ProjectID: "my-project", } mountsIP := []string{ - "filestore://10.0.0.3/share:/data", + "filestore://10.0.0.3/share;/data", } infos1, manifests1, err := smAPIFail.ProcessMounts(mountsIP, job) @@ -719,7 +761,7 @@ func TestProcessMounts_Filestore_Fallback(t *testing.T) { // Case 3: Name Resolution Failure (Name should fail, not fallback) mountsName := []string{ - "filestore://my-filestore-instance/share:/data", + "filestore://my-filestore-instance/share;/data", } _, _, err = smAPIFail.ProcessMounts(mountsName, job) if err == nil { @@ -774,7 +816,7 @@ func TestProcessMounts_Filestore_LocationOverlap(t *testing.T) { ClusterLocation: "europe-west10", } mounts := []string{ - "filestore://my-filestore/share:/data", + "filestore://my-filestore/share;/data", } infos, manifests, err := sm.ProcessMounts(mounts, job) @@ -818,7 +860,7 @@ func TestProcessMounts_Filestore_PVNameLengthLimit(t *testing.T) { ClusterLocation: "us-central1", } mounts := []string{ - "filestore://a-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-name/a-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-share:/data", + "filestore://a-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-name/a-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-very-long-share;/data", } infos, manifests, err := sm.ProcessMounts(mounts, job) @@ -883,8 +925,8 @@ func TestProcessMounts_Filestore_Caching(t *testing.T) { ClusterLocation: "us-central1", } mounts := []string{ - "filestore://inst1/share1:/data1", - "filestore://inst2/share2:/data2", + "filestore://inst1/share1;/data1", + "filestore://inst2/share2;/data2", } _, _, err := sm.ProcessMounts(mounts, job) @@ -927,7 +969,7 @@ func TestProcessMounts_Filestore_IPv6(t *testing.T) { ClusterLocation: "us-central1", } mounts := []string{ - "filestore://[2001:db8::1]/share:/data", + "filestore://[2001:db8::1]/share;/data", } infos, manifests, err := smResolved.ProcessMounts(mounts, job) @@ -966,7 +1008,7 @@ func TestProcessMounts_Filestore_IPv6(t *testing.T) { // Case 3: Invalid IPv6 format in scheme invalidMounts := []string{ - "filestore://[2001:db8::1:invalid]/share:/data", + "filestore://[2001:db8::1:invalid]/share;/data", } _, _, err = smResolved.ProcessMounts(invalidMounts, job) if err == nil { diff --git a/pkg/orchestrator/gke/types.go b/pkg/orchestrator/gke/types.go index 1cbd065994..4ba6a498c4 100644 --- a/pkg/orchestrator/gke/types.go +++ b/pkg/orchestrator/gke/types.go @@ -199,6 +199,7 @@ type MountInfo struct { MountPath string Type string ReadOnly bool + Options string } type FlavorCapacity struct {