Skip to content

Commit fdc4426

Browse files
committed
chore: creating automount objects should not trigger workspace to restart
Signed-off-by: Anatolii Bazko <abazko@redhat.com>
1 parent ec869a7 commit fdc4426

12 files changed

Lines changed: 784 additions & 265 deletions

File tree

controllers/workspace/devworkspace_controller.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2025 Red Hat, Inc.
2+
// Copyright (c) 2019-2026 Red Hat, Inc.
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
@@ -454,11 +454,19 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
454454
return r.failWorkspace(workspace, fmt.Sprintf("Failed to mount SSH askpass script to workspace: %s", err), metrics.ReasonWorkspaceEngineFailure, reqLogger, &reconcileStatus), nil
455455
}
456456

457-
// Add automount resources into devfile containers
458-
// Determine if workspace is transitioning from stopped state
459-
// This is true when the workspace phase is not Starting or Running (i.e., it's Stopped, Failed, or empty)
460-
isWorkspaceStarted := workspace.Status.Phase == dw.DevWorkspaceStatusStarting || workspace.Status.Phase == dw.DevWorkspaceStatusRunning
461-
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, home.PersistUserHomeEnabled(workspace), isWorkspaceStarted)
457+
var workspaceDeployment *appsv1.Deployment
458+
459+
isWorkspaceRunning := workspace.Status.Phase == dw.DevWorkspaceStatusRunning
460+
if !isWorkspaceRunning {
461+
// Fetch the existing deployment to determine whether automount resources with
462+
// `controller.devfile.io/mount-on-start=true` can be mounted without a restart.
463+
// Only needed when the workspace is already running; skip otherwise to reduce API calls.
464+
if workspaceDeployment, err = wsprovision.GetClusterDeployment(workspace, clusterAPI); err != nil {
465+
return reconcile.Result{}, err
466+
}
467+
}
468+
469+
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, home.PersistUserHomeEnabled(workspace), isWorkspaceRunning, workspaceDeployment)
462470
if shouldReturn, reconcileResult, reconcileErr := r.checkDWError(workspace, err, "Failed to process automount resources", metrics.ReasonBadRequest, reqLogger, &reconcileStatus); shouldReturn {
463471
return reconcileResult, reconcileErr
464472
}

pkg/constants/attributes.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2025 Red Hat, Inc.
2+
// Copyright (c) 2019-2026 Red Hat, Inc.
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
@@ -172,9 +172,9 @@ const (
172172
//
173173
WorkspaceRestoreSourceImageAttribute = "controller.devfile.io/restore-source-image"
174174

175-
// MountOnStartOnlyAttribute is an attribute applied to Kubernetes resources to indicate that they should only
176-
// be mounted to a workspace when it starts from a stopped state. When this attribute is set to "true", newly created
177-
// or updated resources will not be automatically mounted to running workspaces, preventing unwanted workspace
175+
// MountOnStartAttribute is an attribute applied to Kubernetes resources to indicate that they should only
176+
// be mounted to a workspace when it starts. When this attribute is set to "true", newly created
177+
// resources will not be automatically mounted to running workspaces, preventing unwanted workspace
178178
// restarts.
179-
MountOnStartOnlyAttribute = "controller.devfile.io/mount-on-start-only"
179+
MountOnStartAttribute = "controller.devfile.io/mount-on-start"
180180
)

pkg/provision/automount/common.go

Lines changed: 93 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2025 Red Hat, Inc.
2+
// Copyright (c) 2019-2026 Red Hat, Inc.
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/devfile/devworkspace-operator/pkg/constants"
2626
"github.com/devfile/devworkspace-operator/pkg/dwerrors"
27+
appsv1 "k8s.io/api/apps/v1"
2728
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
2829

2930
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
@@ -47,9 +48,10 @@ func ProvisionAutoMountResourcesInto(
4748
api sync.ClusterAPI,
4849
namespace string,
4950
persistentHome bool,
50-
isWorkspaceStarted bool,
51+
isWorkspaceRunning bool,
52+
workspaceDeployment *appsv1.Deployment,
5153
) error {
52-
resources, err := getAutomountResources(api, namespace, isWorkspaceStarted)
54+
resources, err := getAutomountResources(api, namespace, isWorkspaceRunning, workspaceDeployment)
5355

5456
if err != nil {
5557
return err
@@ -82,18 +84,23 @@ func ProvisionAutoMountResourcesInto(
8284
return nil
8385
}
8486

85-
func getAutomountResources(api sync.ClusterAPI, namespace string, isWorkspaceStarted bool) (*Resources, error) {
86-
gitCMAutoMountResources, err := ProvisionGitConfiguration(api, namespace, isWorkspaceStarted)
87+
func getAutomountResources(
88+
api sync.ClusterAPI,
89+
namespace string,
90+
isWorkspaceRunning bool,
91+
workspaceDeployment *appsv1.Deployment,
92+
) (*Resources, error) {
93+
gitCMAutoMountResources, err := ProvisionGitConfiguration(api, namespace, isWorkspaceRunning, workspaceDeployment)
8794
if err != nil {
8895
return nil, err
8996
}
9097

91-
cmAutoMountResources, err := getDevWorkspaceConfigmaps(namespace, api, isWorkspaceStarted)
98+
cmAutoMountResources, err := getDevWorkspaceConfigmaps(namespace, api, isWorkspaceRunning, workspaceDeployment)
9299
if err != nil {
93100
return nil, err
94101
}
95102

96-
secretAutoMountResources, err := getDevWorkspaceSecrets(namespace, api, isWorkspaceStarted)
103+
secretAutoMountResources, err := getDevWorkspaceSecrets(namespace, api, isWorkspaceRunning, workspaceDeployment)
97104
if err != nil {
98105
return nil, err
99106
}
@@ -110,7 +117,7 @@ func getAutomountResources(api sync.ClusterAPI, namespace string, isWorkspaceSta
110117
}
111118
dropItemsFieldFromVolumes(mergedResources.Volumes)
112119

113-
pvcAutoMountResources, err := getAutoMountPVCs(namespace, api, isWorkspaceStarted)
120+
pvcAutoMountResources, err := getAutoMountPVCs(namespace, api, isWorkspaceRunning, workspaceDeployment)
114121
if err != nil {
115122
return nil, err
116123
}
@@ -360,3 +367,81 @@ func sortConfigmaps(cms []corev1.ConfigMap) {
360367
return cms[i].Name < cms[j].Name
361368
})
362369
}
370+
371+
func isMountOnStart(obj k8sclient.Object) bool {
372+
return obj.GetAnnotations()[constants.MountOnStartAttribute] == "true"
373+
}
374+
375+
// isAllowedToMount checks whether an automount resource can be added to the workspace pod.
376+
// Resources marked with mount-on-start are only allowed when
377+
// the workspace is not yet running or when they are already present in the current deployment.
378+
func isAllowedToMount(
379+
obj k8sclient.Object,
380+
automountResource Resources,
381+
isWorkspaceRunning bool,
382+
workspaceDeployment *appsv1.Deployment,
383+
) bool {
384+
// No existing deployment to compare against — allow everything
385+
if workspaceDeployment == nil {
386+
return true
387+
}
388+
389+
// Resource without mount-on-start is always eligible
390+
if !isMountOnStart(obj) {
391+
return true
392+
}
393+
394+
// Workspace is not yet running — the pod will be (re)created with these resources included
395+
if !isWorkspaceRunning {
396+
return true
397+
}
398+
399+
// Workspace is already running — only allow if already present in the deployment
400+
return existsInDeployment(automountResource, workspaceDeployment)
401+
}
402+
403+
func existsInDeployment(automountResource Resources, workspaceDeployment *appsv1.Deployment) bool {
404+
return isVolumeMountExistsInDeployment(automountResource, workspaceDeployment) ||
405+
isEnvFromSourceExistsInDeployment(automountResource, workspaceDeployment)
406+
}
407+
408+
// isVolumeMountExistsInDeployment returns true if any volume from the automount resource
409+
// is already present in the workspace deployment's pod spec. Comparison is by name only,
410+
// ignoring VolumeSource — if a name is reused after deleting the old resource, the deletion
411+
// triggers reconciliation and a workspace restart before the new resource is mounted.
412+
func isVolumeMountExistsInDeployment(automountResource Resources, workspaceDeployment *appsv1.Deployment) bool {
413+
for _, automountVolumes := range automountResource.Volumes {
414+
for _, deploymentVolumes := range workspaceDeployment.Spec.Template.Spec.Volumes {
415+
if automountVolumes.Name == deploymentVolumes.Name {
416+
return true
417+
}
418+
}
419+
}
420+
421+
return false
422+
}
423+
424+
// isEnvFromSourceExistsInDeployment returns true if any EnvFromSource from the automount resource
425+
// is already referenced in a container of the workspace deployment, matched by ConfigMap or Secret name.
426+
func isEnvFromSourceExistsInDeployment(automountResource Resources, workspaceDeployment *appsv1.Deployment) bool {
427+
for _, container := range workspaceDeployment.Spec.Template.Spec.Containers {
428+
429+
for _, automountEnvFrom := range automountResource.EnvFromSource {
430+
for _, containerEnvFrom := range container.EnvFrom {
431+
if automountEnvFrom.ConfigMapRef != nil && containerEnvFrom.ConfigMapRef != nil &&
432+
automountEnvFrom.ConfigMapRef.Name == containerEnvFrom.ConfigMapRef.Name {
433+
434+
return true
435+
}
436+
437+
if automountEnvFrom.SecretRef != nil && containerEnvFrom.SecretRef != nil &&
438+
automountEnvFrom.SecretRef.Name == containerEnvFrom.SecretRef.Name {
439+
440+
return true
441+
}
442+
}
443+
}
444+
}
445+
446+
return false
447+
}

pkg/provision/automount/common_persistenthome_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright (c) 2019-2025 Red Hat, Inc.
2+
// Copyright (c) 2019-2026 Red Hat, Inc.
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
55
// You may obtain a copy of the License at
@@ -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, false)
59+
err := ProvisionAutoMountResourcesInto(podAdditions, testAPI, testNamespace, true, false, nil)
6060

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

0 commit comments

Comments
 (0)