Skip to content

Commit fb7d3e4

Browse files
authored
fix(main): restore missed options by cozystack use-case (#326)
2 parents cf88831 + d0c9164 commit fb7d3e4

12 files changed

Lines changed: 639 additions & 4 deletions

api/v1alpha2/cel_validation_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,87 @@ func TestCEL_HappyPathAccepts(t *testing.T) {
574574
}
575575
}
576576

577+
// TestSchema_OptionsValidation exercises the typed spec.options schema
578+
// against a real apiserver: the autoCompactionMode enum, the
579+
// autoCompactionRetention pattern, and the numeric bounds. Not CEL —
580+
// plain OpenAPI validation — but apiserver-enforced all the same, so
581+
// envtest is the seam that actually tests the contract.
582+
func TestSchema_OptionsValidation(t *testing.T) {
583+
skipIfNoEnvtest(t)
584+
ctx := context.Background()
585+
586+
quota := int64(10200547328)
587+
snapCount := int64(10000)
588+
589+
// The exact tuning Cozystack's legacy spec.options carried must be
590+
// accepted in its typed form.
591+
ok := validCluster("opts-cozystack-shape")
592+
ok.Spec.Options = &lll.EtcdOptions{
593+
QuotaBackendBytes: &quota,
594+
AutoCompactionMode: lll.AutoCompactionModePeriodic,
595+
AutoCompactionRetention: "5m",
596+
SnapshotCount: &snapCount,
597+
}
598+
if err := k8s.Create(ctx, ok); err != nil {
599+
t.Fatalf("apiserver rejected valid options: %v", err)
600+
}
601+
t.Cleanup(func() { _ = k8s.Delete(ctx, ok) })
602+
603+
// Bare-integer retention (etcd: hours in periodic mode, revisions in
604+
// revision mode) is also valid.
605+
okInt := validCluster("opts-int-retention")
606+
okInt.Spec.Options = &lll.EtcdOptions{
607+
AutoCompactionMode: lll.AutoCompactionModeRevision,
608+
AutoCompactionRetention: "1000",
609+
}
610+
if err := k8s.Create(ctx, okInt); err != nil {
611+
t.Fatalf("apiserver rejected bare-integer retention: %v", err)
612+
}
613+
t.Cleanup(func() { _ = k8s.Delete(ctx, okInt) })
614+
615+
rejects := []struct {
616+
name string
617+
mut func(*lll.EtcdCluster)
618+
}{
619+
{
620+
name: "bad compaction mode",
621+
mut: func(c *lll.EtcdCluster) {
622+
c.Spec.Options = &lll.EtcdOptions{AutoCompactionMode: "hourly"}
623+
},
624+
},
625+
{
626+
name: "garbage retention",
627+
mut: func(c *lll.EtcdCluster) {
628+
c.Spec.Options = &lll.EtcdOptions{AutoCompactionRetention: "five-minutes"}
629+
},
630+
},
631+
{
632+
name: "negative quota",
633+
mut: func(c *lll.EtcdCluster) {
634+
q := int64(-1)
635+
c.Spec.Options = &lll.EtcdOptions{QuotaBackendBytes: &q}
636+
},
637+
},
638+
{
639+
name: "zero snapshot count",
640+
mut: func(c *lll.EtcdCluster) {
641+
s := int64(0)
642+
c.Spec.Options = &lll.EtcdOptions{SnapshotCount: &s}
643+
},
644+
},
645+
}
646+
for _, tc := range rejects {
647+
t.Run(tc.name, func(t *testing.T) {
648+
c := validCluster("opts-" + strings.ReplaceAll(strings.ToLower(tc.name), " ", "-"))
649+
tc.mut(c)
650+
if err := k8s.Create(ctx, c); err == nil {
651+
_ = k8s.Delete(ctx, c)
652+
t.Fatalf("apiserver accepted invalid options (%s); expected rejection", tc.name)
653+
}
654+
})
655+
}
656+
}
657+
577658
// spec.auth.enabled requires spec.tls.client — auth credentials must
578659
// not cross a plaintext wire. This rule does not reference oldSelf, so it is
579660
// enforced on CREATE.

api/v1alpha2/etcdcluster_types.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,67 @@ type IssuerReference struct {
238238
Kind string `json:"kind,omitempty"`
239239
}
240240

241+
// AutoCompactionMode selects etcd's auto-compaction interpretation of
242+
// the retention value: "periodic" treats it as a time duration,
243+
// "revision" as a revision-count delta.
244+
//
245+
// +kubebuilder:validation:Enum=periodic;revision
246+
type AutoCompactionMode string
247+
248+
const (
249+
// AutoCompactionModePeriodic compacts by time window (--auto-compaction-mode=periodic).
250+
AutoCompactionModePeriodic AutoCompactionMode = "periodic"
251+
// AutoCompactionModeRevision compacts by revision delta (--auto-compaction-mode=revision).
252+
AutoCompactionModeRevision AutoCompactionMode = "revision"
253+
)
254+
255+
// EtcdOptions carries the etcd server tuning flags the operator passes to
256+
// each member's `etcd` command line. Deliberately a closed, typed set — the
257+
// legacy aenix operator exposed a free-form `spec.options` map[string]string,
258+
// which let users inject arbitrary (and operator-conflicting) flags; this
259+
// port types exactly the keys Cozystack's etcd package actually used. New
260+
// flags land here as new typed fields, not as an escape hatch.
261+
//
262+
// Like spec.resources, options are latched through status.observed and take
263+
// effect on newly-created members (scale-up, replacement) only; the operator
264+
// does not roll existing Pods to apply a tuning change in place. Delete one
265+
// Pod at a time to re-template members, or recreate the cluster.
266+
type EtcdOptions struct {
267+
// QuotaBackendBytes sets --quota-backend-bytes: the backend database
268+
// size limit in bytes before the member raises the cluster-wide
269+
// NOSPACE alarm. 0 or absent means etcd's built-in default (2GiB).
270+
// etcd's documented practical maximum is 8GiB.
271+
// +kubebuilder:validation:Minimum=0
272+
// +optional
273+
QuotaBackendBytes *int64 `json:"quotaBackendBytes,omitempty"`
274+
275+
// AutoCompactionMode sets --auto-compaction-mode: how
276+
// AutoCompactionRetention is interpreted, "periodic" (time-based)
277+
// or "revision" (revision-count-based). Absent means etcd's default
278+
// ("periodic" — though compaction only activates when a retention
279+
// is set).
280+
// +optional
281+
AutoCompactionMode AutoCompactionMode `json:"autoCompactionMode,omitempty"`
282+
283+
// AutoCompactionRetention sets --auto-compaction-retention. In
284+
// periodic mode a duration ("5m", "1h"; a bare integer means hours);
285+
// in revision mode a revision count. Absent or "0" disables
286+
// auto-compaction. The pattern admits what etcd itself parses: a
287+
// bare non-negative integer or a Go duration.
288+
// +kubebuilder:validation:Pattern=`^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$|^[0-9]+$`
289+
// +optional
290+
AutoCompactionRetention string `json:"autoCompactionRetention,omitempty"`
291+
292+
// SnapshotCount sets --snapshot-count: the number of committed
293+
// raft entries to retain in memory before triggering an internal
294+
// raft snapshot (this is unrelated to EtcdSnapshot backups). Absent
295+
// means etcd's built-in default. Lower values trade replay speed on
296+
// restart for a smaller memory footprint.
297+
// +kubebuilder:validation:Minimum=1
298+
// +optional
299+
SnapshotCount *int64 `json:"snapshotCount,omitempty"`
300+
}
301+
241302
// Condition types for EtcdCluster.
242303
const (
243304
// ClusterAvailable indicates the cluster has a healthy quorum.
@@ -486,6 +547,17 @@ type EtcdClusterSpec struct {
486547
// the operator does not roll existing Pods to apply a change in place.
487548
// +optional
488549
TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"`
550+
551+
// Options carries etcd server tuning flags (backend quota,
552+
// auto-compaction, raft snapshot count) passed to each member's
553+
// command line. A closed typed set — see EtcdOptions for why there
554+
// is deliberately no free-form flag map.
555+
//
556+
// Updates take effect on newly-created members (scale-up,
557+
// replacement); the operator does not roll existing Pods to apply a
558+
// tuning change in place.
559+
// +optional
560+
Options *EtcdOptions `json:"options,omitempty"`
489561
}
490562

491563
// AdditionalMetadata is a set of labels and annotations the operator merges
@@ -545,6 +617,13 @@ type ObservedClusterSpec struct {
545617
// objects created once the current target is reached.
546618
// +optional
547619
AdditionalMetadata *AdditionalMetadata `json:"additionalMetadata,omitempty"`
620+
621+
// Options is the locked target etcd tuning flags for member Pods.
622+
// Latched with the rest of the target spec so a mid-flight tuning
623+
// edit only applies to members created once the current target is
624+
// reached.
625+
// +optional
626+
Options *EtcdOptions `json:"options,omitempty"`
548627
}
549628

550629
// EtcdClusterStatus defines the observed state of an etcd cluster.

api/v1alpha2/etcdmember_types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@ type EtcdMemberSpec struct {
9898
// +optional
9999
TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"`
100100

101+
// Options mirrors EtcdCluster.spec.options at the time this member
102+
// was created. The member controller renders the set fields as etcd
103+
// command-line flags at Pod-build time; existing members are not
104+
// re-templated when the cluster spec changes.
105+
// +optional
106+
Options *EtcdOptions `json:"options,omitempty"`
107+
101108
// Bootstrap indicates this member is part of the initial cluster formation.
102109
// When true the member starts with --initial-cluster-state=new.
103110
// +optional

api/v1alpha2/zz_generated.deepcopy.go

Lines changed: 40 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/etcd-operator.cozystack.io_etcdclusters.yaml

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,57 @@ spec:
11661166
rule: '!has(self.source.pvc) || (has(self.source.pvc.subPath)
11671167
&& size(self.source.pvc.subPath) > 0)'
11681168
type: object
1169+
options:
1170+
description: |-
1171+
Options carries etcd server tuning flags (backend quota,
1172+
auto-compaction, raft snapshot count) passed to each member's
1173+
command line. A closed typed set — see EtcdOptions for why there
1174+
is deliberately no free-form flag map.
1175+
1176+
Updates take effect on newly-created members (scale-up,
1177+
replacement); the operator does not roll existing Pods to apply a
1178+
tuning change in place.
1179+
properties:
1180+
autoCompactionMode:
1181+
description: |-
1182+
AutoCompactionMode sets --auto-compaction-mode: how
1183+
AutoCompactionRetention is interpreted, "periodic" (time-based)
1184+
or "revision" (revision-count-based). Absent means etcd's default
1185+
("periodic" — though compaction only activates when a retention
1186+
is set).
1187+
enum:
1188+
- periodic
1189+
- revision
1190+
type: string
1191+
autoCompactionRetention:
1192+
description: |-
1193+
AutoCompactionRetention sets --auto-compaction-retention. In
1194+
periodic mode a duration ("5m", "1h"; a bare integer means hours);
1195+
in revision mode a revision count. Absent or "0" disables
1196+
auto-compaction. The pattern admits what etcd itself parses: a
1197+
bare non-negative integer or a Go duration.
1198+
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$|^[0-9]+$
1199+
type: string
1200+
quotaBackendBytes:
1201+
description: |-
1202+
QuotaBackendBytes sets --quota-backend-bytes: the backend database
1203+
size limit in bytes before the member raises the cluster-wide
1204+
NOSPACE alarm. 0 or absent means etcd's built-in default (2GiB).
1205+
etcd's documented practical maximum is 8GiB.
1206+
format: int64
1207+
minimum: 0
1208+
type: integer
1209+
snapshotCount:
1210+
description: |-
1211+
SnapshotCount sets --snapshot-count: the number of committed
1212+
raft entries to retain in memory before triggering an internal
1213+
raft snapshot (this is unrelated to EtcdSnapshot backups). Absent
1214+
means etcd's built-in default. Lower values trade replay speed on
1215+
restart for a smaller memory footprint.
1216+
format: int64
1217+
minimum: 1
1218+
type: integer
1219+
type: object
11691220
progressDeadlineSeconds:
11701221
default: 600
11711222
description: |-
@@ -2805,6 +2856,53 @@ spec:
28052856
x-kubernetes-list-type: atomic
28062857
type: object
28072858
type: object
2859+
options:
2860+
description: |-
2861+
Options is the locked target etcd tuning flags for member Pods.
2862+
Latched with the rest of the target spec so a mid-flight tuning
2863+
edit only applies to members created once the current target is
2864+
reached.
2865+
properties:
2866+
autoCompactionMode:
2867+
description: |-
2868+
AutoCompactionMode sets --auto-compaction-mode: how
2869+
AutoCompactionRetention is interpreted, "periodic" (time-based)
2870+
or "revision" (revision-count-based). Absent means etcd's default
2871+
("periodic" — though compaction only activates when a retention
2872+
is set).
2873+
enum:
2874+
- periodic
2875+
- revision
2876+
type: string
2877+
autoCompactionRetention:
2878+
description: |-
2879+
AutoCompactionRetention sets --auto-compaction-retention. In
2880+
periodic mode a duration ("5m", "1h"; a bare integer means hours);
2881+
in revision mode a revision count. Absent or "0" disables
2882+
auto-compaction. The pattern admits what etcd itself parses: a
2883+
bare non-negative integer or a Go duration.
2884+
pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$|^[0-9]+$
2885+
type: string
2886+
quotaBackendBytes:
2887+
description: |-
2888+
QuotaBackendBytes sets --quota-backend-bytes: the backend database
2889+
size limit in bytes before the member raises the cluster-wide
2890+
NOSPACE alarm. 0 or absent means etcd's built-in default (2GiB).
2891+
etcd's documented practical maximum is 8GiB.
2892+
format: int64
2893+
minimum: 0
2894+
type: integer
2895+
snapshotCount:
2896+
description: |-
2897+
SnapshotCount sets --snapshot-count: the number of committed
2898+
raft entries to retain in memory before triggering an internal
2899+
raft snapshot (this is unrelated to EtcdSnapshot backups). Absent
2900+
means etcd's built-in default. Lower values trade replay speed on
2901+
restart for a smaller memory footprint.
2902+
format: int64
2903+
minimum: 1
2904+
type: integer
2905+
type: object
28082906
replicas:
28092907
description: Replicas is the locked target replica count.
28102908
format: int32

0 commit comments

Comments
 (0)