Skip to content

Commit 99dae5e

Browse files
committed
chore: ephemeral storage requests and limits
Signed-off-by: Artem Bortnikov <brongineer747@gmail.com>
1 parent dc1650b commit 99dae5e

2 files changed

Lines changed: 138 additions & 3 deletions

File tree

internal/controller/factory/backup_job.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package factory
1818

1919
import (
2020
"fmt"
21+
"strconv"
2122
"strings"
2223

2324
etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
@@ -33,8 +34,36 @@ import (
3334
const (
3435
stringTrue = "true"
3536
backupData = "backup-data"
37+
38+
// defaultEtcdQuotaBytes is etcd's default backend quota (2 GiB).
39+
defaultEtcdQuotaBytes int64 = 2 * 1024 * 1024 * 1024
3640
)
3741

42+
// getEffectiveDBQuota returns the maximum etcd DB size for the given cluster,
43+
// used to set ephemeral-storage requests/limits on backup Jobs.
44+
// It checks, in order: explicit quota-backend-bytes option, the full cluster
45+
// storage size, and falls back to etcd's default 2 GiB quota.
46+
func getEffectiveDBQuota(cluster *etcdaenixiov1alpha1.EtcdCluster) resource.Quantity {
47+
// we'll use an explicit quota-backend-bytes option if set.
48+
if v, ok := cluster.Spec.Options["quota-backend-bytes"]; ok && v != "" {
49+
if parsed, err := strconv.ParseInt(v, 10, 64); err == nil && parsed > 0 {
50+
return *resource.NewQuantity(parsed, resource.BinarySI)
51+
}
52+
}
53+
54+
// if a quota-backend-bytes option is not set, use the full cluster storage size
55+
if cluster.Spec.Storage.EmptyDir != nil {
56+
if cluster.Spec.Storage.EmptyDir.SizeLimit != nil {
57+
return *cluster.Spec.Storage.EmptyDir.SizeLimit
58+
}
59+
} else if req := cluster.Spec.Storage.VolumeClaimTemplate.Spec.Resources.Requests.Storage(); req != nil {
60+
return *req
61+
}
62+
63+
// otherwise we'll fall back to etcd's default quota.
64+
return *resource.NewQuantity(defaultEtcdQuotaBytes, resource.BinarySI)
65+
}
66+
3867
// CreateBackupJob builds a Job that runs the backup-agent to take an etcd snapshot
3968
// and store it to the configured destination.
4069
func CreateBackupJob(
@@ -209,6 +238,7 @@ func buildBackupContainer(
209238

210239
envVars = append(envVars, corev1.EnvVar{Name: "BACKUP_INCLUDE_REVISION", Value: stringTrue})
211240

241+
ephemeralStorage := getEffectiveDBQuota(cluster)
212242
container := corev1.Container{
213243
Name: "backup-agent",
214244
Image: operatorImage,
@@ -217,11 +247,13 @@ func buildBackupContainer(
217247
VolumeMounts: volumeMounts,
218248
Resources: corev1.ResourceRequirements{
219249
Requests: corev1.ResourceList{
220-
corev1.ResourceCPU: resource.MustParse("100m"),
221-
corev1.ResourceMemory: resource.MustParse("128Mi"),
250+
corev1.ResourceCPU: resource.MustParse("100m"),
251+
corev1.ResourceMemory: resource.MustParse("128Mi"),
252+
corev1.ResourceEphemeralStorage: ephemeralStorage,
222253
},
223254
Limits: corev1.ResourceList{
224-
corev1.ResourceMemory: resource.MustParse("512Mi"),
255+
corev1.ResourceMemory: resource.MustParse("512Mi"),
256+
corev1.ResourceEphemeralStorage: ephemeralStorage,
225257
},
226258
},
227259
}

internal/controller/factory/backup_job_test.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
etcdaenixiov1alpha1 "github.com/aenix-io/etcd-operator/api/v1alpha1"
2424
corev1 "k8s.io/api/core/v1"
25+
"k8s.io/apimachinery/pkg/api/resource"
2526
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
"k8s.io/apimachinery/pkg/runtime"
2728
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
@@ -116,6 +117,19 @@ func TestCreateBackupJob_PVC(t *testing.T) {
116117
t.Errorf("expected owner name 'my-backup', got %q", job.OwnerReferences[0].Name)
117118
}
118119

120+
// Check ephemeral storage (EmptyDir without SizeLimit → etcd default 2Gi)
121+
expectedEphemeral := resource.NewQuantity(2*1024*1024*1024, resource.BinarySI)
122+
if req, ok := container.Resources.Requests[corev1.ResourceEphemeralStorage]; !ok {
123+
t.Error("ephemeral-storage request not set")
124+
} else if req.Cmp(*expectedEphemeral) != 0 {
125+
t.Errorf("expected ephemeral-storage request %s, got %s", expectedEphemeral.String(), req.String())
126+
}
127+
if lim, ok := container.Resources.Limits[corev1.ResourceEphemeralStorage]; !ok {
128+
t.Error("ephemeral-storage limit not set")
129+
} else if lim.Cmp(*expectedEphemeral) != 0 {
130+
t.Errorf("expected ephemeral-storage limit %s, got %s", expectedEphemeral.String(), lim.String())
131+
}
132+
119133
// Check labels
120134
if job.Labels["etcd.aenix.io/etcdbackup-name"] != "my-backup" {
121135
t.Errorf("expected label etcd.aenix.io/etcdbackup-name=my-backup, got %q", job.Labels["etcd.aenix.io/etcdbackup-name"])
@@ -363,3 +377,92 @@ func TestCreateBackupJob_PVCSubPath(t *testing.T) {
363377
t.Errorf("expected PVC_BACKUP_PATH=%q, got %q", expected, envMap["PVC_BACKUP_PATH"].Value)
364378
}
365379
}
380+
381+
func TestGetEffectiveDBQuota(t *testing.T) {
382+
tests := []struct {
383+
name string
384+
cluster *etcdaenixiov1alpha1.EtcdCluster
385+
expected int64
386+
}{
387+
{
388+
name: "explicit quota-backend-bytes",
389+
cluster: &etcdaenixiov1alpha1.EtcdCluster{
390+
Spec: etcdaenixiov1alpha1.EtcdClusterSpec{
391+
Options: map[string]string{"quota-backend-bytes": "8589934592"}, // 8Gi
392+
Storage: etcdaenixiov1alpha1.StorageSpec{
393+
EmptyDir: &corev1.EmptyDirVolumeSource{},
394+
},
395+
},
396+
},
397+
expected: 8589934592,
398+
},
399+
{
400+
name: "derived from EmptyDir SizeLimit",
401+
cluster: &etcdaenixiov1alpha1.EtcdCluster{
402+
Spec: etcdaenixiov1alpha1.EtcdClusterSpec{
403+
Storage: etcdaenixiov1alpha1.StorageSpec{
404+
EmptyDir: &corev1.EmptyDirVolumeSource{
405+
SizeLimit: ptr.To(resource.MustParse("4Gi")),
406+
},
407+
},
408+
},
409+
},
410+
expected: 4 * 1024 * 1024 * 1024, // 4Gi
411+
},
412+
{
413+
name: "derived from PVC storage request",
414+
cluster: &etcdaenixiov1alpha1.EtcdCluster{
415+
Spec: etcdaenixiov1alpha1.EtcdClusterSpec{
416+
Storage: etcdaenixiov1alpha1.StorageSpec{
417+
VolumeClaimTemplate: etcdaenixiov1alpha1.EmbeddedPersistentVolumeClaim{
418+
Spec: corev1.PersistentVolumeClaimSpec{
419+
Resources: corev1.VolumeResourceRequirements{
420+
Requests: corev1.ResourceList{
421+
corev1.ResourceStorage: resource.MustParse("10Gi"),
422+
},
423+
},
424+
},
425+
},
426+
},
427+
},
428+
},
429+
expected: 10 * 1024 * 1024 * 1024, // 10Gi
430+
},
431+
{
432+
name: "EmptyDir without SizeLimit falls back to etcd default",
433+
cluster: &etcdaenixiov1alpha1.EtcdCluster{
434+
Spec: etcdaenixiov1alpha1.EtcdClusterSpec{
435+
Storage: etcdaenixiov1alpha1.StorageSpec{
436+
EmptyDir: &corev1.EmptyDirVolumeSource{},
437+
},
438+
},
439+
},
440+
expected: 2 * 1024 * 1024 * 1024, // 2Gi
441+
},
442+
{
443+
name: "invalid quota-backend-bytes falls through to storage size",
444+
cluster: &etcdaenixiov1alpha1.EtcdCluster{
445+
Spec: etcdaenixiov1alpha1.EtcdClusterSpec{
446+
Options: map[string]string{"quota-backend-bytes": "not-a-number"},
447+
Storage: etcdaenixiov1alpha1.StorageSpec{
448+
EmptyDir: &corev1.EmptyDirVolumeSource{
449+
SizeLimit: ptr.To(resource.MustParse("4Gi")),
450+
},
451+
},
452+
},
453+
},
454+
expected: 4 * 1024 * 1024 * 1024, // 4Gi
455+
},
456+
}
457+
458+
for _, tt := range tests {
459+
t.Run(tt.name, func(t *testing.T) {
460+
got := getEffectiveDBQuota(tt.cluster)
461+
expectedQ := resource.NewQuantity(tt.expected, resource.BinarySI)
462+
if got.Cmp(*expectedQ) != 0 {
463+
t.Errorf("expected %s (%d bytes), got %s (%d bytes)",
464+
expectedQ.String(), tt.expected, got.String(), got.Value())
465+
}
466+
})
467+
}
468+
}

0 commit comments

Comments
 (0)