Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for ephemeral storageClass #493

Merged
merged 4 commits into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
1 change: 0 additions & 1 deletion pkg/provision/storage/commonStorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
26 changes: 22 additions & 4 deletions pkg/provision/storage/commonStorage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ package storage
import (
"io/ioutil"
"path/filepath"
"sort"
"strings"
"testing"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
Expand Down Expand Up @@ -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(),
Expand All @@ -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")
}
})
Expand Down Expand Up @@ -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
})
}
}
}
61 changes: 61 additions & 0 deletions pkg/provision/storage/ephemeralStorage.go
Original file line number Diff line number Diff line change
@@ -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
}
50 changes: 50 additions & 0 deletions pkg/provision/storage/ephemeralStorage_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
})
}
}
2 changes: 2 additions & 0 deletions pkg/provision/storage/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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: {}