Skip to content

Add support for ephemeral volumes #281

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

Merged
merged 4 commits into from
Mar 1, 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
16 changes: 9 additions & 7 deletions controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,6 @@ func (r *DevWorkspaceReconciler) Reconcile(req ctrl.Request) (reconcileResult ct
return r.stopWorkspace(workspace, reqLogger)
}

// Set finalizer on DevWorkspace if necessary
if ok, err := r.setFinalizer(ctx, workspace); err != nil {
return reconcile.Result{}, err
} else if !ok {
return reconcile.Result{Requeue: true}, nil
}

// Prepare handling workspace status and condition
reconcileStatus := currentStatus{
Conditions: map[devworkspace.WorkspaceConditionType]string{},
Expand Down Expand Up @@ -194,6 +187,15 @@ func (r *DevWorkspaceReconciler) Reconcile(req ctrl.Request) (reconcileResult ct
return reconcile.Result{}, nil
}
workspace.Spec.Template = *flattenedWorkspace
// 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 isFinalizerNecessary(workspace) {
if ok, err := r.setFinalizer(ctx, clusterWorkspace); err != nil {
return reconcile.Result{}, err
} else if !ok {
return reconcile.Result{Requeue: true}, nil
}
}

devfilePodAdditions, err := containerlib.GetKubeContainersFromDevfile(workspace.Spec.Template)
if err != nil {
Expand Down
4 changes: 3 additions & 1 deletion controllers/workspace/finalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ var (
PVCCleanupPodMemoryLimit = resource.MustParse("32Mi")
)

// setFinalizer sets a finalizer on the workspace and syncs the changes to the cluster. No-op if the workspace already
// has the finalizer set.
func (r *DevWorkspaceReconciler) setFinalizer(ctx context.Context, workspace *v1alpha2.DevWorkspace) (ok bool, err error) {
if !isFinalizerNecessary(workspace) || hasFinalizer(workspace) {
if hasFinalizer(workspace) {
return true, nil
}
workspace.SetFinalizers(append(workspace.Finalizers, pvcCleanupFinalizer))
Expand Down
11 changes: 11 additions & 0 deletions pkg/library/container/mountSources.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ func HasMountSources(devfileContainer *devworkspace.ContainerComponent) bool {
return mountSources
}

// AnyMountSources checks HasMountSources for each container component in a devfile. If a component in the slice
// is not a ContainerComponent, it is ignored.
func AnyMountSources(devfileComponents []devworkspace.Component) bool {
for _, component := range devfileComponents {
if component.Container != nil && HasMountSources(component.Container) {
return true
}
}
return false
}

// handleMountSources adds a volumeMount to a container if the corresponding devfile container has
// mountSources enabled.
func handleMountSources(k8sContainer *corev1.Container, devfileContainer *devworkspace.ContainerComponent) {
Expand Down
107 changes: 68 additions & 39 deletions pkg/library/storage/commonStorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ package storage
import (
"fmt"

"k8s.io/apimachinery/pkg/api/resource"

"github.com/devfile/devworkspace-operator/pkg/library/constants"
containerlib "github.com/devfile/devworkspace-operator/pkg/library/container"
corev1 "k8s.io/api/core/v1"
Expand All @@ -39,16 +41,18 @@ import (
//
// Also adds appropriate k8s Volumes to PodAdditions to accomodate the rewritten VolumeMounts.
func RewriteContainerVolumeMounts(workspaceId string, podAdditions *v1alpha1.PodAdditions, workspace devworkspace.DevWorkspaceTemplateSpec) error {
if !NeedsStorage(workspace) {
return nil
}
devfileVolumes := map[string]devworkspace.VolumeComponent{}
var ephemeralVolumes []devworkspace.Component

for _, component := range workspace.Components {
if component.Volume != nil {
if _, exists := devfileVolumes[component.Name]; exists {
return fmt.Errorf("volume component '%s' is defined multiple times", component.Name)
}
devfileVolumes[component.Name] = *component.Volume
if component.Volume.Ephemeral {
ephemeralVolumes = append(ephemeralVolumes, component)
}
}
}
if _, exists := devfileVolumes[constants.ProjectsVolumeName]; !exists {
Expand All @@ -58,56 +62,81 @@ func RewriteContainerVolumeMounts(workspaceId string, podAdditions *v1alpha1.Pod
devfileVolumes[constants.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 {
for cIdx, container := range containers {
for vmIdx, vm := range container.VolumeMounts {
if _, ok := devfileVolumes[vm.Name]; !ok {
return fmt.Errorf("container '%s' references undefined volume '%s'", container.Name, vm.Name)
}
containers[cIdx].VolumeMounts[vmIdx].SubPath = fmt.Sprintf("%s/%s", workspaceId, vm.Name)
containers[cIdx].VolumeMounts[vmIdx].Name = commonPVCName
for _, component := range ephemeralVolumes {
vol := corev1.Volume{
Name: component.Name,
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
}
if component.Volume.Size != "" {
sizeResource, err := resource.ParseQuantity(component.Volume.Size)
if err != nil {
return fmt.Errorf("failed to parse size for Volume %s: %w", component.Name, err)
}
vol.EmptyDir.SizeLimit = &sizeResource
}
return nil
}
if err := rewriteVolumeMounts(podAdditions.Containers); err != nil {
return err
}
if err := rewriteVolumeMounts(podAdditions.InitContainers); err != nil {
return err
podAdditions.Volumes = append(podAdditions.Volumes, vol)
}

podAdditions.Volumes = append(podAdditions.Volumes, corev1.Volume{
Name: commonPVCName,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: commonPVCName,
if NeedsStorage(workspace) {
// 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 {
for cIdx, container := range containers {
for vmIdx, vm := range container.VolumeMounts {
volume, ok := devfileVolumes[vm.Name]
if !ok {
return fmt.Errorf("container '%s' references undefined volume '%s'", container.Name, vm.Name)
}
if !volume.Ephemeral {
containers[cIdx].VolumeMounts[vmIdx].SubPath = fmt.Sprintf("%s/%s", workspaceId, vm.Name)
containers[cIdx].VolumeMounts[vmIdx].Name = commonPVCName
}
}
}
return nil
}
if err := rewriteVolumeMounts(podAdditions.Containers); err != nil {
return err
}
if err := rewriteVolumeMounts(podAdditions.InitContainers); err != nil {
return err
}

podAdditions.Volumes = append(podAdditions.Volumes, corev1.Volume{
Name: commonPVCName,
VolumeSource: corev1.VolumeSource{
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{
ClaimName: commonPVCName,
},
},
},
})
})
}

return nil
}

// NeedsStorage returns true if storage will need to be provisioned for the current workspace
// TODO:
// - This function is used to decide if we need to create a PVC; need to figure out how to handle
// case of ephemeral storage only
// NeedsStorage returns true if storage will need to be provisioned for the current workspace. Note that ephemeral volumes
// do not need to provision storage
func NeedsStorage(workspace devworkspace.DevWorkspaceTemplateSpec) bool {
projectsVolumeIsEphemeral := false
for _, component := range workspace.Components {
if component.Volume != nil {
return true
}
if component.Container != nil {
if len(component.Container.VolumeMounts) > 0 {
// If any non-ephemeral volumes are defined, we need to mount storage
if !component.Volume.Ephemeral {
return true
}
if containerlib.HasMountSources(component.Container) {
return true
if component.Name == constants.ProjectsVolumeName {
projectsVolumeIsEphemeral = component.Volume.Ephemeral
}
}
}
return false
if projectsVolumeIsEphemeral {
// No non-ephemeral volumes, and projects volume mount is ephemeral, so all volumes are ephemeral
return false
}
// Implicit projects volume is non-ephemeral, so any container that mounts sources requires storage
return containerlib.AnyMountSources(workspace.Components)
}
Loading