From f5e245ab2855746ed0692b9bfc7810d88b1fbcdb Mon Sep 17 00:00:00 2001 From: Hernan Guardado <72629357+hernanDatgDev@users.noreply.github.com> Date: Thu, 24 Oct 2024 12:19:45 -0400 Subject: [PATCH] feat(#5864): Added configuration for sizeLimit on empty-dirs in mount trait (#5865) * Added configuration for sizeLimit on empty-dirs in mount trait * refactored emptydir logic to within mount trait * Adding documetation * changing method signature * chore(ci): lint --------- Co-authored-by: hernan-abi Co-authored-by: Pasquale Congiusti --- pkg/apis/camel/v1/trait/mount.go | 3 +- pkg/trait/mount.go | 44 ++++++++++++++++++++---- pkg/trait/mount_test.go | 59 ++++++++++++++++++++++++++++++-- pkg/trait/trait_types.go | 9 ----- pkg/util/resource/config.go | 15 -------- 5 files changed, 96 insertions(+), 34 deletions(-) diff --git a/pkg/apis/camel/v1/trait/mount.go b/pkg/apis/camel/v1/trait/mount.go index 6fa83b745e..9521a05b95 100644 --- a/pkg/apis/camel/v1/trait/mount.go +++ b/pkg/apis/camel/v1/trait/mount.go @@ -36,7 +36,8 @@ type MountTrait struct { // You can use the syntax [pvcname:/container/path:size:accessMode<:storageClass>] to create a dynamic PVC based on the Storage Class provided // or the default cluster Storage Class. However, if the PVC exists, the operator would mount it. Volumes []string `property:"volumes" json:"volumes,omitempty"` - // A list of EmptyDir volumes to be mounted. Syntax: [name:/container/path] + // A list of EmptyDir volumes to be mounted. An optional size limit may be configured (default 500Mi). + // Syntax: name:/container/path[:sizeLimit] EmptyDirs []string `property:"empty-dirs" json:"emptyDirs,omitempty"` // Enable "hot reload" when a secret/configmap mounted is edited (default `false`). The configmap/secret must be // marked with `camel.apache.org/integration` label to be taken in account. The resource will be watched for any kind change, also for diff --git a/pkg/trait/mount.go b/pkg/trait/mount.go index b15c9765d8..38a7703827 100644 --- a/pkg/trait/mount.go +++ b/pkg/trait/mount.go @@ -27,7 +27,6 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" serving "knative.dev/serving/pkg/apis/serving/v1" @@ -40,6 +39,7 @@ import ( "github.com/apache/camel-k/v2/pkg/util/log" "github.com/apache/camel-k/v2/pkg/util/property" utilResource "github.com/apache/camel-k/v2/pkg/util/resource" + "k8s.io/apimachinery/pkg/api/resource" ) const ( @@ -166,11 +166,12 @@ func (t *mountTrait) configureVolumesAndMounts(e *Environment, vols *[]corev1.Vo *mnts = append(*mnts, *volumeMount) } for _, v := range t.EmptyDirs { - if vol, parseErr := utilResource.ParseEmptyDirVolume(v); parseErr == nil { - t.mountResource(vols, mnts, vol) - } else { + volume, volumeMount, parseErr := ParseEmptyDirVolume(v) + if parseErr != nil { return parseErr } + *vols = append(*vols, *volume) + *mnts = append(*mnts, *volumeMount) } return nil @@ -261,8 +262,7 @@ func (t *mountTrait) mountResource(vols *[]corev1.Volume, mnts *[]corev1.VolumeM vol := getVolume(refName, string(conf.StorageType()), conf.Name(), conf.Key(), dstFile) mntPath := getMountPoint(conf.Name(), dstDir, string(conf.StorageType()), string(conf.ContentType())) readOnly := true - if conf.StorageType() == utilResource.StorageTypePVC || - conf.StorageType() == utilResource.StorageTypeEmptyDir { + if conf.StorageType() == utilResource.StorageTypePVC { readOnly = false } mnt := getMount(refName, mntPath, dstFile, readOnly) @@ -281,6 +281,38 @@ func (t *mountTrait) addServiceBindingSecret(e *Environment) { }) } +// ParseEmptyDirVolume will parse and return an empty-dir volume. +func ParseEmptyDirVolume(item string) (*corev1.Volume, *corev1.VolumeMount, error) { + volumeParts := strings.Split(item, ":") + + if len(volumeParts) != 2 && len(volumeParts) != 3 { + return nil, nil, fmt.Errorf("could not match emptyDir volume as %s", item) + } + + refName := kubernetes.SanitizeLabel(volumeParts[0]) + sizeLimit := "500Mi" + if len(volumeParts) == 3 { + sizeLimit = volumeParts[2] + } + + parsed, err := resource.ParseQuantity(sizeLimit) + if err != nil { + return nil, nil, fmt.Errorf("could not parse sizeLimit from emptyDir volume: %s", volumeParts[2]) + } + + volume := &corev1.Volume{ + Name: refName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + SizeLimit: &parsed, + }, + }, + } + + volumeMount := getMount(refName, volumeParts[1], "", false) + return volume, volumeMount, nil +} + // ParseAndCreateVolume will parse a volume configuration. If the volume does not exist it tries to create one based on the storage // class configuration provided or default. // item is expected to be as: name:path/to/mount<:size:accessMode<:storageClassName>>. diff --git a/pkg/trait/mount_test.go b/pkg/trait/mount_test.go index 7013801935..2adaff9a44 100644 --- a/pkg/trait/mount_test.go +++ b/pkg/trait/mount_test.go @@ -136,14 +136,67 @@ func TestEmptyDirVolumeIntegrationPhaseDeploying(t *testing.T) { assert.Len(t, spec.Containers[0].VolumeMounts, 3) assert.Len(t, spec.Volumes, 3) + var emptyDirVolume *corev1.Volume + for _, v := range spec.Volumes { + if v.Name == "my-empty-dir" { + emptyDirVolume = &v + break + } + } + + assert.NotNil(t, emptyDirVolume) + // Default applied by operator + assert.Equal(t, "500Mi", emptyDirVolume.EmptyDir.SizeLimit.String()) + assert.Condition(t, func() bool { - for _, v := range spec.Volumes { - if v.Name == "my-empty-dir" { - return true + for _, container := range spec.Containers { + if container.Name == "integration" { + for _, volumeMount := range container.VolumeMounts { + if volumeMount.Name == "my-empty-dir" { + return true + } + } } } return false }) +} + +func TestEmptyDirVolumeWithSizeLimitIntegrationPhaseDeploying(t *testing.T) { + traitCatalog := NewCatalog(nil) + + environment := getNominalEnv(t, traitCatalog) + environment.Integration.Spec.Traits.Mount = &traitv1.MountTrait{ + EmptyDirs: []string{"my-empty-dir:/some/path:450Mi"}, + } + environment.Platform.ResyncStatusFullConfig() + conditions, traits, err := traitCatalog.apply(environment) + + require.NoError(t, err) + assert.NotEmpty(t, traits) + assert.NotEmpty(t, conditions) + assert.NotEmpty(t, environment.ExecutedTraits) + assert.NotNil(t, environment.GetTrait("mount")) + + deployment := environment.Resources.GetDeployment(func(service *appsv1.Deployment) bool { + return service.Name == "hello" + }) + assert.NotNil(t, deployment) + spec := deployment.Spec.Template.Spec + + assert.Len(t, spec.Containers[0].VolumeMounts, 3) + assert.Len(t, spec.Volumes, 3) + + var emptyDirVolume *corev1.Volume + for _, v := range spec.Volumes { + if v.Name == "my-empty-dir" { + emptyDirVolume = &v + break + } + } + assert.NotNil(t, emptyDirVolume) + assert.Equal(t, "450Mi", emptyDirVolume.EmptyDir.SizeLimit.String()) + assert.Condition(t, func() bool { for _, container := range spec.Containers { if container.Name == "integration" { diff --git a/pkg/trait/trait_types.go b/pkg/trait/trait_types.go index edb3bb2716..ed681f7167 100644 --- a/pkg/trait/trait_types.go +++ b/pkg/trait/trait_types.go @@ -28,7 +28,6 @@ import ( appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" serving "knative.dev/serving/pkg/apis/serving/v1" @@ -440,14 +439,6 @@ func getVolume(volName, storageType, storageName, filterKey, filterValue string) volume.VolumeSource.PersistentVolumeClaim = &corev1.PersistentVolumeClaimVolumeSource{ ClaimName: storageName, } - case emptyDirStorageType: - size, err := resource.ParseQuantity("1Gi") - if err != nil { - log.WithValues("Function", "trait.getVolume").Errorf(err, "could not parse empty dir quantity, skipping") - } - volume.VolumeSource.EmptyDir = &corev1.EmptyDirVolumeSource{ - SizeLimit: &size, - } } return &volume diff --git a/pkg/util/resource/config.go b/pkg/util/resource/config.go index 1bb34af7d0..61c61217f3 100644 --- a/pkg/util/resource/config.go +++ b/pkg/util/resource/config.go @@ -136,21 +136,6 @@ func ParseResource(item string) (*Config, error) { return parse(item, ContentTypeData) } -// ParseEmptyDirVolume will parse an empty dir volume and return a Config. -func ParseEmptyDirVolume(item string) (*Config, error) { - configParts := strings.Split(item, ":") - - if len(configParts) != 2 { - return nil, fmt.Errorf("could not match emptyDir volume as %s", item) - } - - return &Config{ - storageType: StorageTypeEmptyDir, - resourceName: configParts[0], - destinationPath: configParts[1], - }, nil -} - // ParseVolume will parse a volume and return a Config. func ParseVolume(item string) (*Config, error) { configParts := strings.Split(item, ":")