Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/additional-configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,16 @@ By default, resources will be mounted based on the resource name:
Mounting resources can be additionally configured via **annotations**:

* `controller.devfile.io/mount-path`: configure where the resource should be mounted
+
For persistent volume claims, `controller.devfile.io/mount-path` also supports a JSON array to mount multiple subdirectories at different paths:
+
[source,yaml]
----
annotations:
controller.devfile.io/mount-path: '[{"path":"/var/logs","subPath":"data/logs"},{"path":"/etc/config","subPath":"data/config"}]'
----
+
Each entry requires a `path` field (the container mount path) and an optional `subPath` field (the subdirectory within the PVC). An empty or missing annotation falls back to `/tmp/<pvc-name>`.
* `controller.devfile.io/mount-access-mode`: for secrets and configmaps only, configure file permissions on files mounted from this configmap/secret. Permissions can be specified in decimal (e.g. `"511"`) or octal notation by prefixing with a "0" (e.g. `"0777"`)
* `controller.devfile.io/mount-as`: for secrets and configmaps only, configure how the resource should be mounted to the workspace
+
Expand Down
2 changes: 1 addition & 1 deletion pkg/constants/metadata.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2025 Red Hat, Inc.
// Copyright (c) 2019-2026 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
Expand Down
14 changes: 9 additions & 5 deletions pkg/provision/automount/common_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2025 Red Hat, Inc.
// Copyright (c) 2019-2026 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
Expand Down Expand Up @@ -51,10 +51,11 @@ const (
type testCase struct {
Name string `json:"name"`
Input struct {
// Secrets and Configmaps are necessary for deserialization from a testcase
Secrets []corev1.Secret `json:"secrets"`
ConfigMaps []corev1.ConfigMap `json:"configmaps"`
// allObjects contains all Secrets and Configmaps defined above, for convenience
// Secrets, Configmaps, and PVCs are necessary for deserialization from a testcase
Secrets []corev1.Secret `json:"secrets"`
ConfigMaps []corev1.ConfigMap `json:"configmaps"`
PVCs []corev1.PersistentVolumeClaim `json:"pvcs"`
// allObjects contains all Secrets, Configmaps, and PVCs defined above, for convenience
allObjects []client.Object
} `json:"input"`
Output struct {
Expand Down Expand Up @@ -382,6 +383,9 @@ func loadTestCaseOrPanic(t *testing.T, testPath string) testCase {
for idx := range test.Input.Secrets {
test.Input.allObjects = append(test.Input.allObjects, &test.Input.Secrets[idx])
}
for idx := range test.Input.PVCs {
test.Input.allObjects = append(test.Input.allObjects, &test.Input.PVCs[idx])
}

// Overwrite namespace for convenience
for _, obj := range test.Input.allObjects {
Expand Down
66 changes: 51 additions & 15 deletions pkg/provision/automount/pvcs.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright (c) 2019-2025 Red Hat, Inc.
// Copyright (c) 2019-2026 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
Expand All @@ -16,16 +16,51 @@
package automount

import (
"encoding/json"
"fmt"
"path"
"strings"

"github.com/devfile/devworkspace-operator/pkg/provision/sync"
corev1 "k8s.io/api/core/v1"
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"

"github.com/devfile/devworkspace-operator/pkg/common"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
)

type mountPathEntry struct {
Path string `json:"path"`
SubPath string `json:"subPath,omitempty"`
}

func parseMountPathAnnotation(annotation string, pvcName string) ([]mountPathEntry, error) {
if annotation == "" {
return []mountPathEntry{{Path: path.Join("/tmp/", pvcName)}}, nil
}

if !strings.HasPrefix(annotation, "[") {
return []mountPathEntry{{Path: annotation}}, nil
}

var entries []mountPathEntry
if err := json.Unmarshal([]byte(annotation), &entries); err != nil {
return nil, fmt.Errorf("failed to parse mount-path annotation on PVC %s: %w", pvcName, err)
}

if len(entries) == 0 {
return []mountPathEntry{{Path: path.Join("/tmp/", pvcName)}}, nil
}

for i, entry := range entries {
if entry.Path == "" {
return nil, fmt.Errorf("mount-path annotation on PVC %s: entry %d is missing required field 'path'", pvcName, i)
}
}

return entries, nil
}

func getAutoMountPVCs(namespace string, api sync.ClusterAPI) (*Resources, error) {
pvcs := &corev1.PersistentVolumeClaimList{}
if err := api.Client.List(api.Ctx, pvcs, k8sclient.InNamespace(namespace), k8sclient.MatchingLabels{
Expand All @@ -40,15 +75,7 @@ func getAutoMountPVCs(namespace string, api sync.ClusterAPI) (*Resources, error)
var volumes []corev1.Volume
var volumeMounts []corev1.VolumeMount
for _, pvc := range pvcs.Items {
mountPath := pvc.Annotations[constants.DevWorkspaceMountPathAnnotation]
if mountPath == "" {
mountPath = path.Join("/tmp/", pvc.Name)
}

mountReadOnly := false
if pvc.Annotations[constants.DevWorkspaceMountReadyOnlyAnnotation] == "true" {
mountReadOnly = true
}
mountReadOnly := pvc.Annotations[constants.DevWorkspaceMountReadyOnlyAnnotation] == "true"

volumes = append(volumes, corev1.Volume{
Name: common.AutoMountPVCVolumeName(pvc.Name),
Expand All @@ -59,10 +86,19 @@ func getAutoMountPVCs(namespace string, api sync.ClusterAPI) (*Resources, error)
},
},
})
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: common.AutoMountPVCVolumeName(pvc.Name),
MountPath: mountPath,
})

mountPathEntries, err := parseMountPathAnnotation(pvc.Annotations[constants.DevWorkspaceMountPathAnnotation], pvc.Name)
if err != nil {
return nil, err
}

for _, entry := range mountPathEntries {
volumeMounts = append(volumeMounts, corev1.VolumeMount{
Name: common.AutoMountPVCVolumeName(pvc.Name),
MountPath: entry.Path,
SubPath: entry.SubPath,
})
}
}
return &Resources{
Volumes: volumes,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Provisions automount PVC with empty array falls back to default mount path

input:
pvcs:
-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
labels:
controller.devfile.io/mount-to-devworkspace: "true"
annotations:
controller.devfile.io/mount-path: '[]'
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

output:
volumes:
- name: test-pvc
persistentVolumeClaim:
claimName: test-pvc
volumeMounts:
- name: test-pvc
mountPath: /tmp/test-pvc
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Returns error for invalid JSON in mount-path annotation

input:
pvcs:
-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
labels:
controller.devfile.io/mount-to-devworkspace: "true"
annotations:
controller.devfile.io/mount-path: '[{"path":"/data"'
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

output:
errRegexp: "failed to parse mount-path annotation on PVC test-pvc"
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Returns error when path field is missing in mount-path JSON entry

input:
pvcs:
-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
labels:
controller.devfile.io/mount-to-devworkspace: "true"
annotations:
controller.devfile.io/mount-path: '[{"subPath":"data/logs"}]'
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

output:
errRegexp: "mount-path annotation on PVC test-pvc: entry 0 is missing required field 'path'"
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Provisions automount PVC with multiple subpath mounts via JSON array

input:
pvcs:
-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
labels:
controller.devfile.io/mount-to-devworkspace: "true"
annotations:
controller.devfile.io/mount-path: '[{"path":"/var/logs","subPath":"data/logs"},{"path":"/etc/config","subPath":"data/config"}]'
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

output:
volumes:
- name: test-pvc
persistentVolumeClaim:
claimName: test-pvc
volumeMounts:
- name: test-pvc
mountPath: /var/logs
subPath: data/logs
- name: test-pvc
mountPath: /etc/config
subPath: data/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Provisions automount PVC with plain string mount-path

input:
pvcs:
-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
labels:
controller.devfile.io/mount-to-devworkspace: "true"
annotations:
controller.devfile.io/mount-path: /data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

output:
volumes:
- name: test-pvc
persistentVolumeClaim:
claimName: test-pvc
volumeMounts:
- name: test-pvc
mountPath: /data
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: Provisions automount PVC with single entry JSON array

input:
pvcs:
-
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: test-pvc
labels:
controller.devfile.io/mount-to-devworkspace: "true"
annotations:
controller.devfile.io/mount-path: '[{"path":"/data","subPath":"mydir"}]'
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

output:
volumes:
- name: test-pvc
persistentVolumeClaim:
claimName: test-pvc
volumeMounts:
- name: test-pvc
mountPath: /data
subPath: mydir
Loading