Skip to content

Commit ed320b4

Browse files
committed
Merge automount volumes that share mountPath to projected Volume
When multiple automount secrets/configmaps use the same mountPath, merge them into one projected volume referencing those secrets/configmaps to avoid a collision. Signed-off-by: Angel Misevski <amisevsk@redhat.com>
1 parent 2b6de35 commit ed320b4

3 files changed

Lines changed: 169 additions & 1 deletion

File tree

pkg/common/naming.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package common
1717

1818
import (
19+
"crypto/sha256"
1920
"fmt"
2021
"regexp"
2122
"strings"
@@ -130,6 +131,13 @@ func AutoMountPVCVolumeName(pvcName string) string {
130131
return pvcName
131132
}
132133

134+
func AutoMountProjectedVolumeName(mountPath string) string {
135+
// To avoid issues around sanitizing mountPath to generate a unique name (length, allowed chars)
136+
// just use the sha256 hash of mountPath
137+
hash := sha256.Sum256([]byte(mountPath))
138+
return fmt.Sprintf("projected-%x", hash[:10])
139+
}
140+
133141
func WorkspaceRoleName() string {
134142
return "devworkspace-default-role"
135143
}

pkg/provision/automount/common.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,18 @@ func getAutomountResources(api sync.ClusterAPI, namespace string) (*Resources, e
9999
filterGitconfigAutomountVolume(secretAutoMountResources)
100100
}
101101

102+
cmAndSecretResources := mergeAutomountResources(cmAutoMountResources, secretAutoMountResources)
103+
mergedResources, err := mergeProjectedVolumes(cmAndSecretResources)
104+
if err != nil {
105+
return nil, err
106+
}
107+
102108
pvcAutoMountResources, err := getAutoMountPVCs(namespace, api)
103109
if err != nil {
104110
return nil, err
105111
}
106112

107-
return mergeAutomountResources(gitCMAutoMountResources, cmAutoMountResources, secretAutoMountResources, pvcAutoMountResources), nil
113+
return mergeAutomountResources(gitCMAutoMountResources, mergedResources, pvcAutoMountResources), nil
108114
}
109115

110116
func checkAutomountVolumesForCollision(podAdditions *v1alpha1.PodAdditions, automount *Resources) error {
@@ -172,6 +178,8 @@ func formatVolumeDescription(vol corev1.Volume) string {
172178
return fmt.Sprintf("secret '%s'", vol.Secret.SecretName)
173179
} else if vol.ConfigMap != nil {
174180
return fmt.Sprintf("configmap '%s'", vol.ConfigMap.Name)
181+
} else if vol.PersistentVolumeClaim != nil {
182+
return fmt.Sprintf("pvc '%s'", vol.PersistentVolumeClaim.ClaimName)
175183
}
176184
return fmt.Sprintf("'%s'", vol.Name)
177185
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) 2019-2023 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package automount
15+
16+
import (
17+
"fmt"
18+
"sort"
19+
"strings"
20+
21+
"github.com/devfile/devworkspace-operator/pkg/common"
22+
corev1 "k8s.io/api/core/v1"
23+
"k8s.io/utils/pointer"
24+
)
25+
26+
// mergeProjectedVolumes merges secret and configmap automount resources that share a mount path
27+
// into projected volumes.
28+
func mergeProjectedVolumes(resources *Resources) (*Resources, error) {
29+
mergedResources := &Resources{}
30+
31+
// Map of mountPath -> volumeMount, to detect colliding volumeMounts
32+
mountPathToVolumeMounts := map[string][]corev1.VolumeMount{}
33+
needsProjectedVolume := false
34+
// Ordered list of mountPaths to process, to avoid random iteration order on maps
35+
var mountPathOrder []string
36+
for _, volumeMount := range resources.VolumeMounts {
37+
if len(mountPathToVolumeMounts[volumeMount.MountPath]) == 0 {
38+
mountPathOrder = append(mountPathOrder, volumeMount.MountPath)
39+
} else {
40+
needsProjectedVolume = true
41+
}
42+
mountPathToVolumeMounts[volumeMount.MountPath] = append(mountPathToVolumeMounts[volumeMount.MountPath], volumeMount)
43+
}
44+
if !needsProjectedVolume {
45+
// Return early and do nothing if we didn't find a mountPath collision above
46+
return resources, nil
47+
}
48+
sort.Strings(mountPathOrder)
49+
50+
// Map of volume names -> volumes, for easier lookup
51+
volumeNameToVolume := map[string]corev1.Volume{}
52+
for _, volume := range resources.Volumes {
53+
volumeNameToVolume[volume.Name] = volume
54+
}
55+
56+
for _, mountPath := range mountPathOrder {
57+
volumeMounts := mountPathToVolumeMounts[mountPath]
58+
switch len(volumeMounts) {
59+
case 0:
60+
continue
61+
case 1:
62+
// No projected volume necessary
63+
mergedResources.VolumeMounts = append(mergedResources.VolumeMounts, volumeMounts[0])
64+
volume := volumeNameToVolume[volumeMounts[0].Name]
65+
mergedResources.Volumes = append(mergedResources.Volumes, volume)
66+
default:
67+
vm, vol, err := generateProjectedVolume(mountPath, volumeMounts, volumeNameToVolume)
68+
if err != nil {
69+
return nil, err
70+
}
71+
mergedResources.VolumeMounts = append(mergedResources.VolumeMounts, *vm)
72+
mergedResources.Volumes = append(mergedResources.Volumes, *vol)
73+
}
74+
}
75+
76+
mergedResources.EnvFromSource = resources.EnvFromSource
77+
78+
return mergedResources, nil
79+
}
80+
81+
// generateProjectedVolume creates a projected Volume and VolumeMount that should be used in place multiple VolumeMounts
82+
// with the same mountPath.
83+
func generateProjectedVolume(mountPath string, volumeMounts []corev1.VolumeMount, volumeNameToVolume map[string]corev1.Volume) (*corev1.VolumeMount, *corev1.Volume, error) {
84+
volumeName := common.AutoMountProjectedVolumeName(mountPath)
85+
projectedVolume := &corev1.Volume{
86+
Name: volumeName,
87+
VolumeSource: corev1.VolumeSource{
88+
Projected: &corev1.ProjectedVolumeSource{
89+
DefaultMode: pointer.Int32(0640),
90+
},
91+
},
92+
}
93+
94+
for _, vm := range volumeMounts {
95+
if err := checkCanUseProjectedVolumes(volumeMounts, volumeNameToVolume); err != nil {
96+
return nil, nil, err
97+
}
98+
99+
volume := volumeNameToVolume[vm.Name]
100+
projection := corev1.VolumeProjection{}
101+
switch {
102+
case volume.Secret != nil:
103+
projection.Secret = &corev1.SecretProjection{
104+
LocalObjectReference: corev1.LocalObjectReference{
105+
Name: volume.Secret.SecretName,
106+
},
107+
}
108+
case volume.ConfigMap != nil:
109+
projection.ConfigMap = &corev1.ConfigMapProjection{
110+
LocalObjectReference: corev1.LocalObjectReference{
111+
Name: volume.ConfigMap.Name,
112+
},
113+
}
114+
default:
115+
return nil, nil, fmt.Errorf("unrecognized volume type for volume %s", volume.Name)
116+
}
117+
projectedVolume.Projected.Sources = append(projectedVolume.Projected.Sources, projection)
118+
}
119+
120+
projectedVolumeMount := &corev1.VolumeMount{
121+
Name: volumeName,
122+
MountPath: mountPath,
123+
ReadOnly: true,
124+
}
125+
126+
return projectedVolumeMount, projectedVolume, nil
127+
}
128+
129+
// checkCanProjectedVolumes checks whether a set of volumeMounts (assumed to share the same mountPath) can be merged
130+
// into a single VolumeMount and projected Volume. Returns an error if VolumeMounts should not be merged.
131+
func checkCanUseProjectedVolumes(volumeMounts []corev1.VolumeMount, volumeNameToVolume map[string]corev1.Volume) error {
132+
isError := false
133+
for _, vm := range volumeMounts {
134+
// If any of the volumeMounts is using a subPath (and mountPaths collide) this is not an issue we can fix. This shouldn't
135+
// happen often with automount volumes as it would require e.g. two configmaps with the same mountPath and key
136+
if vm.SubPath != "" || vm.SubPathExpr != "" {
137+
isError = true
138+
}
139+
vol := volumeNameToVolume[vm.Name]
140+
if vol.PersistentVolumeClaim != nil {
141+
isError = true
142+
}
143+
}
144+
if isError {
145+
var problemNames []string
146+
for _, vm := range volumeMounts {
147+
problemNames = append(problemNames, formatVolumeDescription(volumeNameToVolume[vm.Name]))
148+
}
149+
return fmt.Errorf("auto-mounted volumes from (%s) have the same mount path", strings.Join(problemNames, ", "))
150+
}
151+
return nil
152+
}

0 commit comments

Comments
 (0)