Skip to content

Commit

Permalink
Support persistent home directory
Browse files Browse the repository at this point in the history
This commit allows the `/home/user/` directory in workspaces to  persist if  persistent storage is enabled
(through the use of the 'per-user' ('common') or 'per-workspace' storage class).

To enable this feature, modify the DWOC and set `config.workspace.persistUserHome.enabled` to `true`.

When enabled, a Devfile volume named `persistentHome` will be added to DevWorkspaces. All DevWorkspace container
components will mount the `persistentHome` volume at `/home/user/`. If a container component of a DevWorkspace already
mounts to `/home/user/`, the DWO-provisioned `persistentHome` volume will not be added to the DevWorkspace.

Fix devfile#1097

Signed-off-by: Andrew Obuchowicz <aobuchow@redhat.com>
  • Loading branch information
AObuchow committed May 30, 2023
1 parent bad287c commit 5c393d7
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 0 deletions.
4 changes: 4 additions & 0 deletions controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,10 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
return r.failWorkspace(workspace, fmt.Sprintf("Error provisioning storage: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil
}

if wsprovision.NeedsPersistentHomeDirectory(workspace) {
wsprovision.AddHomeVolume(&workspace.Spec.Template)
}

// Set finalizer on DevWorkspace if necessary
// Note: we need to check the flattened workspace to see if a finalizer is needed, as plugins could require storage
if storageProvisioner.NeedsStorage(&workspace.Spec.Template) && !controllerutil.ContainsFinalizer(clusterWorkspace, constants.StorageCleanupFinalizer) {
Expand Down
66 changes: 66 additions & 0 deletions pkg/provision/workspace/persistentHome.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// Copyright (c) 2019-2023 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package workspace

import (
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"

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

// Adds a Devfile volume to the DevWorkspaceTemplateSpec and mounts the volume to `/home/user/` for every
// container component defined in the DevWorkspaceTemplateSpec by adding additional volumeMounts.
// The DevWorkspaceTemplateSpec remains unchanged if any of its container components already mount a volume to `/home/user/`.
func AddHomeVolume(devWorkspaceTemplateSpec *v1alpha2.DevWorkspaceTemplateSpec) {
homeUserDirPath := "/home/user/"
homeVolumeName := "persistentHome"

homeVolume := v1alpha2.Component{
Name: homeVolumeName,
ComponentUnion: v1alpha2.ComponentUnion{
Volume: &v1alpha2.VolumeComponent{},
},
}
homeVolumeMount := v1alpha2.VolumeMount{
Name: homeVolumeName,
Path: homeUserDirPath,
}

for _, component := range devWorkspaceTemplateSpec.Components {
if component.Container == nil {
continue
}
for _, volumeMount := range component.Container.VolumeMounts {
if volumeMount.Path == homeUserDirPath {
// If a volume is already being mounted to /home/user/, it takes precedence
// over the DWO-provisioned home directory volume.
return
}
}
}

devWorkspaceTemplateSpec.Components = append(devWorkspaceTemplateSpec.Components, homeVolume)
for _, component := range devWorkspaceTemplateSpec.Components {
if component.Container == nil {
continue
}
component.Container.VolumeMounts = append(component.Container.VolumeMounts, homeVolumeMount)
}
}

func NeedsPersistentHomeDirectory(workspace *common.DevWorkspaceWithConfig) bool {
return *workspace.Config.Workspace.PersistUserHome.Enabled
}
105 changes: 105 additions & 0 deletions pkg/provision/workspace/persistentHome_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
//
// Copyright (c) 2019-2023 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
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

package workspace

import (
"os"
"path/filepath"
"testing"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/devfile/devworkspace-operator/pkg/common"
"github.com/google/go-cmp/cmp"
"sigs.k8s.io/yaml"

"github.com/stretchr/testify/assert"
)

type testCase struct {
Name string `json:"name,omitempty"`
Input testInput `json:"input,omitempty"`
Output testOutput `json:"output,omitempty"`
}

type testInput struct {
DevWorkspaceID string `json:"devworkspaceId,omitempty"`
Workspace *dw.DevWorkspaceTemplateSpec `json:"workspace,omitempty"`
Config *v1alpha1.OperatorConfiguration `json:"config,omitempty"`
}

type testOutput struct {
Workspace *dw.DevWorkspaceTemplateSpec `json:"workspace,omitempty"`
}

func loadTestCaseOrPanic(t *testing.T, testFilepath string) testCase {
bytes, err := os.ReadFile(testFilepath)
if err != nil {
t.Fatal(err)
}
var test testCase
if err := yaml.Unmarshal(bytes, &test); err != nil {
t.Fatal(err)
}
return test
}

func loadAllTestCasesOrPanic(t *testing.T, fromDir string) []testCase {
files, err := os.ReadDir(fromDir)
if err != nil {
t.Fatal(err)
}
var tests []testCase
for _, file := range files {
if file.IsDir() {
continue
}
tests = append(tests, loadTestCaseOrPanic(t, filepath.Join(fromDir, file.Name())))
}
return tests
}

func getDevWorkspaceWithConfig(input testInput) *common.DevWorkspaceWithConfig {
return &common.DevWorkspaceWithConfig{
DevWorkspace: &dw.DevWorkspace{
Spec: dw.DevWorkspaceSpec{
Template: *input.Workspace,
},
},
Config: input.Config,
}
}

func TestPersistentHomeVolume(t *testing.T) {
tests := loadAllTestCasesOrPanic(t, "testdata/persistent-home")
for _, tt := range tests {
t.Run(tt.Name, func(t *testing.T) {
// sanity check that file is read correctly.
assert.NotNil(t, tt.Input.Workspace, "Input does not define workspace")
assert.NotNil(t, tt.Input.Config, "Input does not define a config")
workspace := getDevWorkspaceWithConfig(tt.Input)

if NeedsPersistentHomeDirectory(workspace) {
AddHomeVolume(&workspace.Spec.Template)
}

assert.Equal(t, tt.Output.Workspace.DevWorkspaceTemplateSpecContent, workspace.Spec.Template.DevWorkspaceTemplateSpecContent,
"DevWorkspace Template Spec should match expected output: Diff: %s",
cmp.Diff(tt.Output.Workspace.DevWorkspaceTemplateSpecContent, workspace.Spec.Template.DevWorkspaceTemplateSpecContent))
})
}

}

0 comments on commit 5c393d7

Please sign in to comment.