Skip to content

Commit 3aa293d

Browse files
authored
feat: mount ConfigMaps/Secrets/PVCs only to a specific DevWorkspace (#1619)
Signed-off-by: Anatolii Bazko <abazko@redhat.com>
1 parent fa371eb commit 3aa293d

17 files changed

Lines changed: 1309 additions & 87 deletions

controllers/workspace/devworkspace_controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -465,7 +465,7 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
465465
}
466466
}
467467

468-
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, home.PersistUserHomeEnabled(workspace), workspaceDeployment)
468+
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, workspace.Name, home.PersistUserHomeEnabled(workspace), workspaceDeployment)
469469
if shouldReturn, reconcileResult, reconcileErr := r.checkDWError(workspace, err, "Failed to process automount resources", metrics.ReasonBadRequest, reqLogger, &reconcileStatus); shouldReturn {
470470
return reconcileResult, reconcileErr
471471
}

controllers/workspace/eventhandlers.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2121
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
2222
"github.com/devfile/devworkspace-operator/pkg/constants"
23+
"github.com/devfile/devworkspace-operator/pkg/provision/automount"
2324
"k8s.io/apimachinery/pkg/types"
2425
"sigs.k8s.io/controller-runtime/pkg/client"
2526
"sigs.k8s.io/controller-runtime/pkg/reconcile"
@@ -118,22 +119,30 @@ func (r *DevWorkspaceReconciler) dwPVCHandler(ctx context.Context, obj client.Ob
118119
return reconciles
119120
}
120121

121-
func (r *DevWorkspaceReconciler) runningWorkspacesHandler(ctx context.Context, obj client.Object) []reconcile.Request {
122+
func (r *DevWorkspaceReconciler) runningWorkspacesHandler(_ context.Context, obj client.Object) []reconcile.Request {
122123
dwList := &dw.DevWorkspaceList{}
123124
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
124125
return []reconcile.Request{}
125126
}
126127
var reconciles []reconcile.Request
127128
for _, workspace := range dwList.Items {
128-
// Queue reconciles for any started workspaces to make sure they pick up new object
129-
if workspace.Spec.Started {
130-
reconciles = append(reconciles, reconcile.Request{
131-
NamespacedName: types.NamespacedName{
132-
Name: workspace.GetName(),
133-
Namespace: workspace.GetNamespace(),
134-
},
135-
})
129+
// Queue reconciles ONLY for any started workspaces to make sure they pick up new object
130+
if !workspace.Spec.Started {
131+
continue
136132
}
133+
134+
// Skip workspaces that don't match the resource's include/exclude annotations
135+
// to avoid unnecessary reconciles in multi-workspace clusters.
136+
if !automount.MatchesWorkspaceTarget(obj, workspace.GetName()) {
137+
continue
138+
}
139+
140+
reconciles = append(reconciles, reconcile.Request{
141+
NamespacedName: types.NamespacedName{
142+
Name: workspace.GetName(),
143+
Namespace: workspace.GetNamespace(),
144+
},
145+
})
137146
}
138147
return reconciles
139148
}

docs/additional-configuration.adoc

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,58 @@ When "file" is used, the configmap is mounted as a directory within the workspac
168168
169169
* `controller.devfile.io/mount-on-start`: when set to `"true"`, the resource will only be mounted when a workspace starts. If the resource is created while a workspace is already running, it will not be automatically mounted until the workspace is restarted. This prevents unwanted workspace restarts caused by newly created automount resources. This annotation can be applied to configmaps, secrets, and persistent volume claims.
170170
+
171-
For git credential secrets (labelled with `controller.devfile.io/git-credential`) and git TLS configmaps (labelled with `controller.devfile.io/git-tls-credential`), the annotation is evaluated across all git credential resources as a group, not individually. Since all git credentials are merged into a single mounted secret, if at least one git credential secret (or TLS configmap) lacks the `controller.devfile.io/mount-on-start` annotation, all git credentials (or TLS configmaps) will be mounted, including those marked with `mount-on-start`.
171+
For git credential secrets (labeled with `controller.devfile.io/git-credential`)
172+
and git TLS configmaps (labelled with `controller.devfile.io/git-tls-credential`),
173+
this annotation behaves differently. The `controller.devfile.io/mount-on-start` annotation is evaluated across
174+
all git credential resources as a group, not individually. Since all git credentials are
175+
merged into a single mounted secret, if at least one git credential secret (or TLS configmap)
176+
lacks the `controller.devfile.io/mount-on-start` annotation, all git credentials
177+
(or TLS configmaps) will be mounted, including those marked with `controller.devfile.io/mount-on-start`.
178+
179+
* `controller.devfile.io/mount-to-devworkspace-include`: configure which DevWorkspaces the
180+
resource should be mounted to. The value is a comma-separated list of patterns matched against
181+
the DevWorkspace name. The resource is only mounted to workspaces whose name matches at
182+
least one pattern. This annotation can be applied to ConfigMaps, Secrets and Persistent Volume Claims.
183+
+
184+
* `controller.devfile.io/mount-to-devworkspace-exclude`: configure which DevWorkspaces the
185+
resource should NOT be mounted to. The value is a comma-separated list of patterns
186+
matched against the DevWorkspace name. The resource is not mounted to workspaces whose name
187+
matches any pattern. This annotation can be applied to ConfigMaps, Secrets and Persistent Volume Claims.
188+
+
189+
Supported patterns for both annotations:
190+
+
191+
--
192+
** `name` -- exact match
193+
** `name*` -- prefix match
194+
** `*name` -- suffix match
195+
** `\*name*` -- contains match
196+
** `*` -- matches all workspaces
197+
--
198+
+
199+
Both annotations can be used at the same time on the same resource. When both are present, the resource is mounted only to workspaces that match the include pattern AND do not match the exclude pattern.
200+
If neither annotation is present, the resource is mounted to all workspaces (default behavior).
201+
+
202+
[source,yaml]
203+
----
204+
metadata:
205+
annotations:
206+
controller.devfile.io/mount-to-devworkspace-include: "my-workspace, staging-*"
207+
----
208+
+
209+
[source,yaml]
210+
----
211+
metadata:
212+
annotations:
213+
controller.devfile.io/mount-to-devworkspace-exclude: "temp-*, *-test"
214+
----
215+
+
216+
[source,yaml]
217+
----
218+
metadata:
219+
annotations:
220+
controller.devfile.io/mount-to-devworkspace-include: "staging-*"
221+
controller.devfile.io/mount-to-devworkspace-exclude: "staging-legacy"
222+
----
172223
+
173224
[source,yaml]
174225
----
@@ -201,6 +252,19 @@ data:
201252
202253
This will mount a file `/tmp/.git-credentials/credentials` in all workspace containers, and construct a git config to use this file as a credentials store.
203254
255+
### Using a base gitconfig
256+
If an automount configmap or secret (labeled with `controller.devfile.io/automount: true`)
257+
is mounted as `subpath` and contains a key that resolves to `/etc/gitconfig`,
258+
its contents are used as the base git configuration for the workspace.
259+
This base gitconfig is merged with the generated git credentials configuration.
260+
261+
For example, a configmap with `controller.devfile.io/mount-path: /etc` and a key named
262+
`gitconfig` will be detected and used as the base git configuration.
263+
264+
The `controller.devfile.io/mount-to-devworkspace-include` and
265+
`controller.devfile.io/mount-to-devworkspace-exclude` annotations are respected,
266+
allowing the base gitconfig to be targeted to specific workspaces.
267+
204268
## Configuring DevWorkspaces to use SSH keys for Git operations
205269
Git SSH keys can be configured for DevWorkspaces by mounting secrets to workspaces.
206270

pkg/constants/metadata.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,28 @@ const (
4949
// DevWorkspaceMountLabel is the label key to store if a configmap, secret, or PVC should be mounted to the devworkspace
5050
DevWorkspaceMountLabel = "controller.devfile.io/mount-to-devworkspace"
5151

52+
// DevWorkspaceMountIncludeAnnotation is an annotation to configure which DevWorkspaces an automount
53+
// resource should be mounted to. The value is a comma-separated list of patterns matched against
54+
// the DevWorkspace name. The resource is only mounted to workspaces whose name matches at least one pattern.
55+
// Supported patterns:
56+
// - exact match `name`
57+
// - prefix match `name*`
58+
// - suffix match `*name`
59+
// - contains match `*name*`
60+
// - matches all workspaces `*`
61+
DevWorkspaceMountIncludeAnnotation = "controller.devfile.io/mount-to-devworkspace-include"
62+
63+
// DevWorkspaceMountExcludeAnnotation is an annotation to configure which DevWorkspaces an automount
64+
// resource should NOT be mounted to. The value is a comma-separated list of patterns matched against
65+
// the DevWorkspace name. The resource is not mounted to workspaces whose name matches any pattern.
66+
// Supported patterns:
67+
// - exact match `name`
68+
// - prefix match `name*`
69+
// - suffix match `*name`
70+
// - contains match `*name*`
71+
// - matches all workspaces `*`
72+
DevWorkspaceMountExcludeAnnotation = "controller.devfile.io/mount-to-devworkspace-exclude"
73+
5274
// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
5375
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
5476
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>

pkg/provision/automount/common.go

Lines changed: 87 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ type Resources struct {
4646
func ProvisionAutoMountResourcesInto(
4747
podAdditions *v1alpha1.PodAdditions,
4848
api sync.ClusterAPI,
49-
namespace string,
49+
workspaceNamespace string,
50+
workspaceName string,
5051
persistentHome bool,
5152
workspaceDeployment *appsv1.Deployment,
5253
) error {
53-
resources, err := getAutomountResources(api, namespace, workspaceDeployment)
54+
resources, err := getAutomountResources(api, workspaceNamespace, workspaceName, workspaceDeployment)
5455

5556
if err != nil {
5657
return err
@@ -85,20 +86,21 @@ func ProvisionAutoMountResourcesInto(
8586

8687
func getAutomountResources(
8788
api sync.ClusterAPI,
88-
namespace string,
89+
workspaceNamespace string,
90+
workspaceName string,
8991
workspaceDeployment *appsv1.Deployment,
9092
) (*Resources, error) {
91-
gitCMAutoMountResources, err := ProvisionGitConfiguration(api, namespace, workspaceDeployment)
93+
gitCMAutoMountResources, err := ProvisionGitConfiguration(api, workspaceNamespace, workspaceName, workspaceDeployment)
9294
if err != nil {
9395
return nil, err
9496
}
9597

96-
cmAutoMountResources, err := getDevWorkspaceConfigmaps(namespace, api, workspaceDeployment)
98+
cmAutoMountResources, err := getDevWorkspaceConfigmaps(workspaceNamespace, workspaceName, api, workspaceDeployment)
9799
if err != nil {
98100
return nil, err
99101
}
100102

101-
secretAutoMountResources, err := getDevWorkspaceSecrets(namespace, api, workspaceDeployment)
103+
secretAutoMountResources, err := getDevWorkspaceSecrets(workspaceNamespace, workspaceName, api, workspaceDeployment)
102104
if err != nil {
103105
return nil, err
104106
}
@@ -115,7 +117,7 @@ func getAutomountResources(
115117
}
116118
dropItemsFieldFromVolumes(mergedResources.Volumes)
117119

118-
pvcAutoMountResources, err := getAutoMountPVCs(namespace, api, workspaceDeployment)
120+
pvcAutoMountResources, err := getAutoMountPVCs(workspaceNamespace, workspaceName, api, workspaceDeployment)
119121
if err != nil {
120122
return nil, err
121123
}
@@ -214,12 +216,17 @@ func flattenAutomountResources(resources []Resources) Resources {
214216
return flattened
215217
}
216218

217-
// findGitconfigAutomount searches a namespace for a automount resource (configmap or secret) that contains
219+
// findGitconfigAutomount searches a namespace for an automount resource (configmap or secret) that contains
218220
// a system-wide gitconfig (i.e. the mountpath is `/etc/gitconfig`). Only objects with mount type "subpath"
219-
// are considered. If a suitable object is found, the contents of the gitconfig defined there is returned.
220-
func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *string, err error) {
221+
// are considered. Resources that do not match the workspace's include/exclude annotations are skipped.
222+
// If a suitable object is found, the contents of the gitconfig defined there is returned.
223+
func findGitconfigAutomount(
224+
api sync.ClusterAPI,
225+
workspaceNamespace string,
226+
workspaceName string,
227+
) (gitconfig *string, err error) {
221228
configmapList := &corev1.ConfigMapList{}
222-
if err := api.Client.List(api.Ctx, configmapList, k8sclient.InNamespace(namespace), k8sclient.MatchingLabels{
229+
if err := api.Client.List(api.Ctx, configmapList, k8sclient.InNamespace(workspaceNamespace), k8sclient.MatchingLabels{
223230
constants.DevWorkspaceMountLabel: "true",
224231
}); err != nil {
225232
return nil, err
@@ -228,6 +235,13 @@ func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *s
228235
if cm.Annotations[constants.DevWorkspaceMountAsAnnotation] != constants.DevWorkspaceMountAsSubpath {
229236
continue
230237
}
238+
239+
// Filter resources by workspace name
240+
if !MatchesWorkspaceTarget(&cm, workspaceName) {
241+
log.V(1).Info("Skipping ConfigMap, workspace does not match include/exclude annotations", "namespace", cm.Namespace, "name", cm.Name, "workspace", workspaceName)
242+
continue
243+
}
244+
231245
mountPath := cm.Annotations[constants.DevWorkspaceMountPathAnnotation]
232246
for key, value := range cm.Data {
233247
if path.Join(mountPath, key) == "/etc/gitconfig" {
@@ -240,7 +254,7 @@ func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *s
240254
}
241255

242256
secretList := &corev1.SecretList{}
243-
if err := api.Client.List(api.Ctx, secretList, k8sclient.InNamespace(namespace), k8sclient.MatchingLabels{
257+
if err := api.Client.List(api.Ctx, secretList, k8sclient.InNamespace(workspaceNamespace), k8sclient.MatchingLabels{
244258
constants.DevWorkspaceMountLabel: "true",
245259
}); err != nil {
246260
return nil, err
@@ -249,6 +263,13 @@ func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *s
249263
if secret.Annotations[constants.DevWorkspaceMountAsAnnotation] != constants.DevWorkspaceMountAsSubpath {
250264
continue
251265
}
266+
267+
// Filter resources by workspace name
268+
if !MatchesWorkspaceTarget(&secret, workspaceName) {
269+
log.V(1).Info("Skipping Secret, workspace does not match include/exclude annotations", "namespace", secret.Namespace, "name", secret.Name, "workspace", workspaceName)
270+
continue
271+
}
272+
252273
mountPath := secret.Annotations[constants.DevWorkspaceMountPathAnnotation]
253274
for key, value := range secret.Data {
254275
if path.Join(mountPath, key) == "/etc/gitconfig" {
@@ -370,10 +391,10 @@ func isMountOnStart(obj k8sclient.Object) bool {
370391
return obj.GetAnnotations()[constants.MountOnStartAttribute] == "true"
371392
}
372393

373-
// isAllowedToMount checks whether an automount resource can be added to the workspace pod.
394+
// canMountWithoutRestart checks whether an automount resource can be added to the workspace pod.
374395
// Resources marked with mount-on-start are only allowed when
375396
// the workspace is not yet running or when they are already present in the current deployment.
376-
func isAllowedToMount(
397+
func canMountWithoutRestart(
377398
obj k8sclient.Object,
378399
automountResource Resources,
379400
workspaceDeployment *appsv1.Deployment,
@@ -437,3 +458,55 @@ func isEnvFromSourceExistsInDeployment(automountResource Resources, workspaceDep
437458

438459
return false
439460
}
461+
462+
// MatchesWorkspaceTarget checks whether a resource should be mounted to a given workspace
463+
// based on include/exclude annotations. Both annotations can be used together: the resource
464+
// is mounted when it matches the include pattern (or no include is set) and does not match the exclude pattern.
465+
func MatchesWorkspaceTarget(
466+
obj k8sclient.Object,
467+
workspaceName string,
468+
) bool {
469+
annotations := obj.GetAnnotations()
470+
471+
includePatterns := strings.TrimSpace(annotations[constants.DevWorkspaceMountIncludeAnnotation])
472+
excludePatterns := strings.TrimSpace(annotations[constants.DevWorkspaceMountExcludeAnnotation])
473+
474+
included := includePatterns == "" || matchesAnyPattern(includePatterns, workspaceName)
475+
excluded := excludePatterns != "" && matchesAnyPattern(excludePatterns, workspaceName)
476+
477+
return included && !excluded
478+
}
479+
480+
func matchesAnyPattern(patternsStr string, workspaceName string) bool {
481+
patterns := strings.Split(patternsStr, ",")
482+
for _, pattern := range patterns {
483+
pattern = strings.TrimSpace(pattern)
484+
if pattern == "" {
485+
continue
486+
}
487+
488+
if pattern == "*" {
489+
return true
490+
}
491+
492+
startsWithWildcard := strings.HasPrefix(pattern, "*")
493+
endsWithWildcard := strings.HasSuffix(pattern, "*")
494+
495+
var matched bool
496+
switch {
497+
case startsWithWildcard && endsWithWildcard:
498+
matched = strings.Contains(workspaceName, pattern[1:len(pattern)-1])
499+
case endsWithWildcard:
500+
matched = strings.HasPrefix(workspaceName, pattern[:len(pattern)-1])
501+
case startsWithWildcard:
502+
matched = strings.HasSuffix(workspaceName, pattern[1:])
503+
default:
504+
matched = workspaceName == pattern
505+
}
506+
507+
if matched {
508+
return true
509+
}
510+
}
511+
return false
512+
}

pkg/provision/automount/common_persistenthome_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func TestProvisionAutomountResourcesIntoPersistentHomeEnabled(t *testing.T) {
5656
Client: fake.NewClientBuilder().WithObjects(tt.Input.allObjects...).Build(),
5757
}
5858

59-
err := ProvisionAutoMountResourcesInto(podAdditions, testAPI, testNamespace, true, nil)
59+
err := ProvisionAutoMountResourcesInto(podAdditions, testAPI, testNamespace, "test-workspace", true, nil)
6060

6161
if !assert.NoError(t, err, "Unexpected error") {
6262
return

0 commit comments

Comments
 (0)