diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index eb1712712..b225cf286 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -66,4 +66,7 @@ const ( // AsyncStorageClassType defines the 'asynchronous' storage policy. An rsync sidecar is added to devworkspaces that uses SSH to connect // to a storage deployment that mounts a common PVC for the namespace. AsyncStorageClassType = "async" + // EphemeralStorageClassType defines the 'ephemeral' storage policy: all volumes are allocated as emptyDir volumes and + // so do not require cleanup. When a DevWorkspace is stopped, all local changes are lost. + EphemeralStorageClassType = "ephemeral" ) diff --git a/pkg/provision/storage/commonStorage.go b/pkg/provision/storage/commonStorage.go index d0c173902..810d9ee59 100644 --- a/pkg/provision/storage/commonStorage.go +++ b/pkg/provision/storage/commonStorage.go @@ -88,7 +88,6 @@ func (p *CommonStorageProvisioner) rewriteContainerVolumeMounts(workspaceId stri devfileVolumes[devfileConstants.ProjectsVolumeName] = projectsVolume } - // TODO: Support more than the common PVC strategy here (storage provisioner interface?) // TODO: What should we do when a volume isn't explicitly defined? commonPVCName := config.ControllerCfg.GetWorkspacePVCName() rewriteVolumeMounts := func(containers []corev1.Container) error { diff --git a/pkg/provision/storage/commonStorage_test.go b/pkg/provision/storage/commonStorage_test.go index f8eee621c..c9fc03487 100644 --- a/pkg/provision/storage/commonStorage_test.go +++ b/pkg/provision/storage/commonStorage_test.go @@ -15,6 +15,8 @@ package storage import ( "io/ioutil" "path/filepath" + "sort" + "strings" "testing" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" @@ -95,16 +97,15 @@ func loadAllTestCasesOrPanic(t *testing.T, fromDir string) []testCase { return tests } -func TestRewriteContainerVolumeMounts(t *testing.T) { - tests := loadAllTestCasesOrPanic(t, "testdata") - // tests := []testCase{loadTestCaseOrPanic(t, "testdata/can-make-projects-ephemeral.yaml")} +func TestRewriteContainerVolumeMountsForCommonStorageClass(t *testing.T) { + tests := loadAllTestCasesOrPanic(t, "testdata/common-storage") setupControllerCfg() commonStorage := CommonStorageProvisioner{} commonPVC, err := getCommonPVCSpec("test-namespace") - commonPVC.Status.Phase = corev1.ClaimBound if err != nil { t.Fatalf("Failure during setup: %s", err) } + commonPVC.Status.Phase = corev1.ClaimBound clusterAPI := provision.ClusterAPI{ Client: fake.NewFakeClientWithScheme(scheme, commonPVC), Logger: zap.New(), @@ -125,6 +126,8 @@ func TestRewriteContainerVolumeMounts(t *testing.T) { if !assert.NoError(t, err, "Should not return error") { return } + sortVolumesAndVolumeMounts(&tt.Output.PodAdditions) + sortVolumesAndVolumeMounts(&tt.Input.PodAdditions) assert.Equal(t, tt.Output.PodAdditions, tt.Input.PodAdditions, "PodAdditions should match expected output") } }) @@ -236,3 +239,18 @@ func TestNeedsStorage(t *testing.T) { }) } } + +func sortVolumesAndVolumeMounts(podAdditions *v1alpha1.PodAdditions) { + if podAdditions.Volumes != nil { + sort.Slice(podAdditions.Volumes, func(i, j int) bool { + return strings.Compare(podAdditions.Volumes[i].Name, podAdditions.Volumes[j].Name) < 0 + }) + } + for idx, container := range podAdditions.Containers { + if container.VolumeMounts != nil { + sort.Slice(podAdditions.Containers[idx].VolumeMounts, func(i, j int) bool { + return strings.Compare(podAdditions.Containers[idx].VolumeMounts[i].Name, podAdditions.Containers[idx].VolumeMounts[j].Name) < 0 + }) + } + } +} diff --git a/pkg/provision/storage/ephemeralStorage.go b/pkg/provision/storage/ephemeralStorage.go new file mode 100644 index 000000000..a4ddbcbd3 --- /dev/null +++ b/pkg/provision/storage/ephemeralStorage.go @@ -0,0 +1,61 @@ +// +// Copyright (c) 2019-2021 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package storage + +import ( + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + + "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1" + "github.com/devfile/devworkspace-operator/controllers/workspace/provision" + "github.com/devfile/devworkspace-operator/pkg/library/container" +) + +// The EphemeralStorageProvisioner provisions all workspace storage as emptyDir volumes. +// Any local changes are lost when the workspace is stopped; its lifetime is tied to the +// underlying pod. +type EphemeralStorageProvisioner struct{} + +var _ Provisioner = (*EphemeralStorageProvisioner)(nil) + +func (e EphemeralStorageProvisioner) NeedsStorage(_ *dw.DevWorkspaceTemplateSpec) bool { + // Since all volumes are emptyDir, no PVC needs to be provisioned + return false +} + +func (e EphemeralStorageProvisioner) ProvisionStorage(podAdditions *v1alpha1.PodAdditions, workspace *dw.DevWorkspace, _ provision.ClusterAPI) error { + persistent, ephemeral, projects := getWorkspaceVolumes(workspace) + if _, err := addEphemeralVolumesToPodAdditions(podAdditions, persistent); err != nil { + return err + } + if _, err := addEphemeralVolumesToPodAdditions(podAdditions, ephemeral); err != nil { + return err + } + if projects != nil { + if _, err := addEphemeralVolumesToPodAdditions(podAdditions, []dw.Component{*projects}); err != nil { + return err + } + } else { + if container.AnyMountSources(workspace.Spec.Template.Components) { + projectsComponent := dw.Component{Name: "projects"} + projectsComponent.Volume = &dw.VolumeComponent{} + if _, err := addEphemeralVolumesToPodAdditions(podAdditions, []dw.Component{projectsComponent}); err != nil { + return err + } + } + } + return nil +} + +func (e EphemeralStorageProvisioner) CleanupWorkspaceStorage(_ *dw.DevWorkspace, _ provision.ClusterAPI) error { + return nil +} diff --git a/pkg/provision/storage/ephemeralStorage_test.go b/pkg/provision/storage/ephemeralStorage_test.go new file mode 100644 index 000000000..5d0a5b828 --- /dev/null +++ b/pkg/provision/storage/ephemeralStorage_test.go @@ -0,0 +1,50 @@ +// +// Copyright (c) 2019-2021 Red Hat, Inc. +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +// Contributors: +// Red Hat, Inc. - initial API and implementation +// + +package storage + +import ( + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" + + "github.com/devfile/devworkspace-operator/controllers/workspace/provision" +) + +func TestRewriteContainerVolumeMountsForEphemeralStorageClass(t *testing.T) { + tests := loadAllTestCasesOrPanic(t, "testdata/ephemeral-storage") + setupControllerCfg() + commonStorage := EphemeralStorageProvisioner{} + + 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") + workspace := &dw.DevWorkspace{} + workspace.Spec.Template = *tt.Input.Workspace + workspace.Status.DevWorkspaceId = tt.Input.DevWorkspaceID + workspace.Namespace = "test-namespace" + err := commonStorage.ProvisionStorage(&tt.Input.PodAdditions, workspace, provision.ClusterAPI{}) + if tt.Output.ErrRegexp != nil && assert.Error(t, err) { + assert.Regexp(t, *tt.Output.ErrRegexp, err.Error(), "Error message should match") + } else { + if !assert.NoError(t, err, "Should not return error") { + return + } + sortVolumesAndVolumeMounts(&tt.Output.PodAdditions) + sortVolumesAndVolumeMounts(&tt.Input.PodAdditions) + assert.Equal(t, tt.Output.PodAdditions, tt.Input.PodAdditions, "PodAdditions should match expected output") + } + }) + } +} diff --git a/pkg/provision/storage/provisioner.go b/pkg/provision/storage/provisioner.go index 9e6e3f432..e53833bdd 100644 --- a/pkg/provision/storage/provisioner.go +++ b/pkg/provision/storage/provisioner.go @@ -49,6 +49,8 @@ func GetProvisioner(workspace *dw.DevWorkspace) (Provisioner, error) { return &CommonStorageProvisioner{}, nil case constants.AsyncStorageClassType: return &AsyncStorageProvisioner{}, nil + case constants.EphemeralStorageClassType: + return &EphemeralStorageProvisioner{}, nil default: return nil, UnsupportedStorageStrategy } diff --git a/pkg/provision/storage/testdata/can-make-projects-ephemeral.yaml b/pkg/provision/storage/testdata/common-storage/can-make-projects-ephemeral.yaml similarity index 100% rename from pkg/provision/storage/testdata/can-make-projects-ephemeral.yaml rename to pkg/provision/storage/testdata/common-storage/can-make-projects-ephemeral.yaml diff --git a/pkg/provision/storage/testdata/can-set-ephemeral-volume-size.yaml b/pkg/provision/storage/testdata/common-storage/can-set-ephemeral-volume-size.yaml similarity index 100% rename from pkg/provision/storage/testdata/can-set-ephemeral-volume-size.yaml rename to pkg/provision/storage/testdata/common-storage/can-set-ephemeral-volume-size.yaml diff --git a/pkg/provision/storage/testdata/does-nothing-for-no-storage-needed.yaml b/pkg/provision/storage/testdata/common-storage/does-nothing-for-no-storage-needed.yaml similarity index 100% rename from pkg/provision/storage/testdata/does-nothing-for-no-storage-needed.yaml rename to pkg/provision/storage/testdata/common-storage/does-nothing-for-no-storage-needed.yaml diff --git a/pkg/provision/storage/testdata/error-duplicate-volumes.yaml b/pkg/provision/storage/testdata/common-storage/error-duplicate-volumes.yaml similarity index 100% rename from pkg/provision/storage/testdata/error-duplicate-volumes.yaml rename to pkg/provision/storage/testdata/common-storage/error-duplicate-volumes.yaml diff --git a/pkg/provision/storage/testdata/error-undefined-volume-init-container.yaml b/pkg/provision/storage/testdata/common-storage/error-undefined-volume-init-container.yaml similarity index 100% rename from pkg/provision/storage/testdata/error-undefined-volume-init-container.yaml rename to pkg/provision/storage/testdata/common-storage/error-undefined-volume-init-container.yaml diff --git a/pkg/provision/storage/testdata/error-undefined-volume.yaml b/pkg/provision/storage/testdata/common-storage/error-undefined-volume.yaml similarity index 100% rename from pkg/provision/storage/testdata/error-undefined-volume.yaml rename to pkg/provision/storage/testdata/common-storage/error-undefined-volume.yaml diff --git a/pkg/provision/storage/testdata/error-unparseable-ephemeral-size.yaml b/pkg/provision/storage/testdata/common-storage/error-unparseable-ephemeral-size.yaml similarity index 100% rename from pkg/provision/storage/testdata/error-unparseable-ephemeral-size.yaml rename to pkg/provision/storage/testdata/common-storage/error-unparseable-ephemeral-size.yaml diff --git a/pkg/provision/storage/testdata/handles-ephemeral-volumes.yaml b/pkg/provision/storage/testdata/common-storage/handles-ephemeral-volumes.yaml similarity index 100% rename from pkg/provision/storage/testdata/handles-ephemeral-volumes.yaml rename to pkg/provision/storage/testdata/common-storage/handles-ephemeral-volumes.yaml diff --git a/pkg/provision/storage/testdata/handles-projects-volume-ordering.yaml b/pkg/provision/storage/testdata/common-storage/handles-projects-volume-ordering.yaml similarity index 100% rename from pkg/provision/storage/testdata/handles-projects-volume-ordering.yaml rename to pkg/provision/storage/testdata/common-storage/handles-projects-volume-ordering.yaml diff --git a/pkg/provision/storage/testdata/projects-volume-overriding.yaml b/pkg/provision/storage/testdata/common-storage/projects-volume-overriding.yaml similarity index 100% rename from pkg/provision/storage/testdata/projects-volume-overriding.yaml rename to pkg/provision/storage/testdata/common-storage/projects-volume-overriding.yaml diff --git a/pkg/provision/storage/testdata/rewrites-volumes-for-common-pvc-strategy.yaml b/pkg/provision/storage/testdata/common-storage/rewrites-volumes-for-common-pvc-strategy.yaml similarity index 100% rename from pkg/provision/storage/testdata/rewrites-volumes-for-common-pvc-strategy.yaml rename to pkg/provision/storage/testdata/common-storage/rewrites-volumes-for-common-pvc-strategy.yaml diff --git a/pkg/provision/storage/testdata/ephemeral-storage/supports-ephemeral-storageclass.yaml b/pkg/provision/storage/testdata/ephemeral-storage/supports-ephemeral-storageclass.yaml new file mode 100644 index 000000000..aaffb7650 --- /dev/null +++ b/pkg/provision/storage/testdata/ephemeral-storage/supports-ephemeral-storageclass.yaml @@ -0,0 +1,68 @@ +name: "Supports ephemeral storageclass" + +input: + devworkspaceId: "test-workspaceid" + podAdditions: + containers: + - name: testing-container-1 + image: testing-image-1 + volumeMounts: + - name: testvol-1 + mountPath: testPath1 + - name: testvol-3 + mountPath: testPath3 + - name: testing-container-2 + image: testing-image-2 + volumeMounts: + - name: testvol-2 + mountPath: testPath2 + - name: "projects" + mountPath: "/projects" + + workspace: + components: + - name: testing-container-1 + container: + image: testing-image-1 + mountSources: false + - name: testing-container-2 + container: + image: testing-image-2 + mountSources: true + + - name: testvol-1 + volume: + ephemeral: true + - name: testvol-2 + volume: + ephemeral: false + - name: testvol-3 + volume: {} + +output: + podAdditions: + containers: + - name: testing-container-1 + image: testing-image-1 + volumeMounts: + - name: testvol-1 + mountPath: testPath1 + - name: testvol-3 + mountPath: testPath3 + - name: testing-container-2 + image: testing-image-2 + volumeMounts: + - name: testvol-2 + mountPath: testPath2 + - name: projects + mountPath: /projects + + volumes: + - name: projects + emptyDir: {} + - name: testvol-1 + emptyDir: {} + - name: testvol-2 + emptyDir: {} + - name: testvol-3 + emptyDir: {}