Skip to content

Commit 9361cb4

Browse files
committed
review fixes
1 parent 562a61f commit 9361cb4

20 files changed

Lines changed: 733 additions & 408 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
bin
99
# kubectl-etcd plugin: build to bin/ (see Makefile); never commit a root-level build artifact
1010
/kubectl-etcd
11+
# etcd-migrate tool: same rule — build to bin/, never commit the root-level artifact
12+
/etcd-migrate
1113

1214
# Test binary, build with `go test -c`
1315
*.test

api/v1alpha2/cel_validation_test.go

Lines changed: 0 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -655,66 +655,6 @@ func TestSchema_OptionsValidation(t *testing.T) {
655655
}
656656
}
657657

658-
// TestCEL_HeadlessServiceNameImmutable covers the adoption field's
659-
// lifecycle rules: valid on create, rejected on add, change and removal —
660-
// the name is baked into member identity (peer URLs persisted in etcd,
661-
// pod subdomains), so any post-create edit would desync DNS from etcd.
662-
func TestCEL_HeadlessServiceNameImmutable(t *testing.T) {
663-
skipIfNoEnvtest(t)
664-
ctx := context.Background()
665-
666-
// Valid on create (the in-place migration path).
667-
c := validCluster("hsn-create")
668-
c.Spec.HeadlessServiceName = "legacy-headless"
669-
if err := k8s.Create(ctx, c); err != nil {
670-
t.Fatalf("create with headlessServiceName: %v", err)
671-
}
672-
t.Cleanup(func() { _ = k8s.Delete(ctx, c) })
673-
674-
// Change rejected.
675-
got := &lll.EtcdCluster{}
676-
if err := k8s.Get(ctx, ctrlclient.ObjectKeyFromObject(c), got); err != nil {
677-
t.Fatalf("Get: %v", err)
678-
}
679-
got.Spec.HeadlessServiceName = "other"
680-
if err := k8s.Update(ctx, got); err == nil {
681-
t.Fatalf("apiserver accepted headlessServiceName change; expected rejection")
682-
} else if !strings.Contains(err.Error(), "immutable") {
683-
t.Fatalf("error did not mention immutability: %v", err)
684-
}
685-
686-
// Removal rejected.
687-
if err := k8s.Get(ctx, ctrlclient.ObjectKeyFromObject(c), got); err != nil {
688-
t.Fatalf("Get: %v", err)
689-
}
690-
got.Spec.HeadlessServiceName = ""
691-
if err := k8s.Update(ctx, got); err == nil {
692-
t.Fatalf("apiserver accepted headlessServiceName removal; expected rejection")
693-
}
694-
695-
// Add-on-existing rejected.
696-
plain := validCluster("hsn-add")
697-
if err := k8s.Create(ctx, plain); err != nil {
698-
t.Fatalf("create plain cluster: %v", err)
699-
}
700-
t.Cleanup(func() { _ = k8s.Delete(ctx, plain) })
701-
if err := k8s.Get(ctx, ctrlclient.ObjectKeyFromObject(plain), got); err != nil {
702-
t.Fatalf("Get: %v", err)
703-
}
704-
got.Spec.HeadlessServiceName = "late-override"
705-
if err := k8s.Update(ctx, got); err == nil {
706-
t.Fatalf("apiserver accepted adding headlessServiceName post-create; expected rejection")
707-
}
708-
709-
// DNS-label pattern enforced.
710-
bad := validCluster("hsn-bad")
711-
bad.Spec.HeadlessServiceName = "Not_A_DNS_Label"
712-
if err := k8s.Create(ctx, bad); err == nil {
713-
_ = k8s.Delete(ctx, bad)
714-
t.Fatalf("apiserver accepted invalid headlessServiceName; expected rejection")
715-
}
716-
}
717-
718658
// spec.auth.enabled requires spec.tls.client — auth credentials must
719659
// not cross a plaintext wire. This rule does not reference oldSelf, so it is
720660
// enforced on CREATE.

api/v1alpha2/etcdcluster_types.go

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -435,8 +435,6 @@ type StorageSpec struct {
435435
// +kubebuilder:validation:XValidation:rule="!has(self.auth) || !has(oldSelf.auth) || self.auth == oldSelf.auth",message="spec.auth is immutable post-create; delete and recreate the cluster to change auth configuration"
436436
// +kubebuilder:validation:XValidation:rule="!(has(self.auth) && self.auth.enabled) || (has(self.tls) && has(self.tls.client))",message="spec.auth.enabled requires spec.tls.client (auth credentials must not cross a plaintext connection)"
437437
// +kubebuilder:validation:XValidation:rule="!(has(self.auth) && self.auth.enabled) || has(self.auth.rootCredentialsSecretRef)",message="spec.auth.enabled requires spec.auth.rootCredentialsSecretRef"
438-
// +kubebuilder:validation:XValidation:rule="has(self.headlessServiceName) == has(oldSelf.headlessServiceName)",message="spec.headlessServiceName cannot be added to or removed from an existing cluster; it is baked into member identity (peer URLs, pod subdomains)"
439-
// +kubebuilder:validation:XValidation:rule="!has(self.headlessServiceName) || !has(oldSelf.headlessServiceName) || self.headlessServiceName == oldSelf.headlessServiceName",message="spec.headlessServiceName is immutable post-create; it is baked into member identity (peer URLs, pod subdomains)"
440438
// +kubebuilder:validation:XValidation:rule="has(self.bootstrap) == has(oldSelf.bootstrap)",message="spec.bootstrap cannot be added to or removed from an existing cluster; it is consulted only at first bootstrap"
441439
// +kubebuilder:validation:XValidation:rule="!has(self.bootstrap) || !has(oldSelf.bootstrap) || self.bootstrap == oldSelf.bootstrap",message="spec.bootstrap is immutable post-create; it is consulted only at first bootstrap"
442440
// +kubebuilder:validation:XValidation:rule="!(has(self.bootstrap) && has(self.bootstrap.restore)) || !(has(self.storage) && has(self.storage.medium) && self.storage.medium == 'Memory')",message="spec.bootstrap.restore is unsupported with spec.storage.medium=Memory: the restored data dir is tmpfs, so any seed Pod restart re-restores the snapshot — reverting writes (single member) or breaking the cluster with a fresh ID it can't rejoin (multi-member). Use persistent storage to restore."
@@ -560,27 +558,6 @@ type EtcdClusterSpec struct {
560558
// tuning change in place.
561559
// +optional
562560
Options *EtcdOptions `json:"options,omitempty"`
563-
564-
// HeadlessServiceName overrides the name of the headless Service the
565-
// operator maintains for per-member DNS. Empty (the default) means
566-
// the Service is named after the cluster. The name is baked into
567-
// every member's identity — peer URLs persisted inside etcd, the
568-
// Pods' spec.subdomain, and every endpoint the operator dials — so
569-
// the field is immutable post-create (spec-level CEL, like
570-
// storageClassName).
571-
//
572-
// The main consumer is in-place migration from the legacy
573-
// etcd.aenix.io operator: its StatefulSet pods carry an immutable
574-
// spec.subdomain of "<cluster>-headless" and their peer URLs are
575-
// persisted in etcd under that DNS domain. Setting
576-
// headlessServiceName to the legacy Service name makes the operator's
577-
// constructed URLs match the adopted pods' actual DNS exactly, so
578-
// discovery, scale-up and replacement work without any
579-
// adopted-member special cases.
580-
// +kubebuilder:validation:MaxLength=63
581-
// +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$`
582-
// +optional
583-
HeadlessServiceName string `json:"headlessServiceName,omitempty"`
584561
}
585562

586563
// AdditionalMetadata is a set of labels and annotations the operator merges

api/v1alpha2/etcdmember_types.go

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -105,32 +105,6 @@ type EtcdMemberSpec struct {
105105
// +optional
106106
Options *EtcdOptions `json:"options,omitempty"`
107107

108-
// HeadlessServiceName mirrors EtcdCluster.spec.headlessServiceName.
109-
// Empty means the headless Service is named after the cluster. The
110-
// member controller uses it for the Pod's spec.subdomain and for
111-
// every constructed peer/client URL, so the member's DNS identity
112-
// matches the cluster's headless Service.
113-
// +kubebuilder:validation:MaxLength=63
114-
// +kubebuilder:validation:Pattern=`^[a-z]([-a-z0-9]*[a-z0-9])?$`
115-
// +optional
116-
HeadlessServiceName string `json:"headlessServiceName,omitempty"`
117-
118-
// DataDirSubPath relocates etcd's --data-dir to a subdirectory of the
119-
// member's data volume: empty (the default) means the volume root
120-
// (/var/lib/etcd), a value V means /var/lib/etcd/V. A single path
121-
// component — no slashes — so it cannot escape the mount.
122-
//
123-
// Set by the in-place migration from the legacy etcd.aenix.io
124-
// operator, whose StatefulSet pods kept their data under the
125-
// "default.etcd" subdirectory of the PVC. With the subPath recorded
126-
// here, a replacement Pod built by this operator finds the existing
127-
// data dir and resumes with the member's persisted identity instead
128-
// of starting empty and crashlooping against the cluster.
129-
// +kubebuilder:validation:MaxLength=128
130-
// +kubebuilder:validation:Pattern=`^[a-zA-Z0-9][a-zA-Z0-9._-]*$`
131-
// +optional
132-
DataDirSubPath string `json:"dataDirSubPath,omitempty"`
133-
134108
// Bootstrap indicates this member is part of the initial cluster formation.
135109
// When true the member starts with --initial-cluster-state=new.
136110
// +optional

0 commit comments

Comments
 (0)