diff --git a/README.md b/README.md index 2bf78daf..abe4cbd6 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ The full design rationale is in [docs/concepts.md](docs/concepts.md). - **PodDisruptionBudget**: per-cluster PDB selects voting members only (`role=voter`); `minAvailable` is the quorum of whichever is larger, the live voter count or the intended cluster size, so `kubectl drain` cannot voluntarily push the cluster below quorum — even while node churn shrinks live membership. - **TLS (BYO Secrets or cert-manager)**: `spec.tls.client` / `spec.tls.peer` enable TLS on each surface independently. Material comes from either user-provided Secrets (`serverSecretRef` / `operatorClientSecretRef` / `secretRef`) or operator-emitted `cert-manager.io/v1` Certificates (`certManager.{serverIssuerRef,operatorClientIssuerRef,issuerRef}`) — mutually exclusive per subtree, enforced by CEL. mTLS is the implicit mode when an operator-client source is supplied; server-TLS-only when it isn't. The whole `tls` subtree is CEL-locked immutable post-create. cert-manager-emitted certs auto-renew via cert-manager; Pod-side rotation is a manual one-at-a-time `kubectl delete pod` either way. See [docs/concepts.md](docs/concepts.md#tls). - **Resource sizing**: `spec.resources` (a `corev1.ResourceRequirements`) sets the etcd container's CPU/memory requests and limits. Unset uses a conservative 100m/128Mi-request default. Updates take effect on newly-created members; pair with a `VerticalPodAutoscaler` targeting the cluster for live recommendation/rollout. -- **Scheduling & extra metadata**: `spec.affinity` and `spec.topologySpreadConstraints` pass through to every member Pod (anti-affinity is not defaulted — set it for production); `spec.additionalMetadata` merges user labels/annotations onto every object the operator creates (member Pods, data PVCs, Services, PDB, `EtcdMember` CRs), with operator-owned keys winning on collision. All three apply on object creation and are latched like the rest of the spec. See [docs/concepts.md](docs/concepts.md#pod-scheduling-and-additional-metadata). +- **Scheduling & extra metadata**: `spec.affinity`, `spec.topologySpreadConstraints` and `spec.tolerations` pass through to every member Pod (anti-affinity is not defaulted — set it for production); `spec.additionalMetadata` merges user labels/annotations onto every object the operator creates (member Pods, data PVCs, Services, PDB, `EtcdMember` CRs), with operator-owned keys winning on collision. All of these apply on object creation and are latched like the rest of the spec. See [docs/concepts.md](docs/concepts.md#pod-scheduling-and-additional-metadata). - **Zone-local clients**: the `etcd-operator.cozystack.io/client-service-traffic-distribution` annotation on an `EtcdCluster` (`PreferClose` / `PreferSameZone` / `PreferSameNode`) sets `trafficDistribution` on the `-client` Service, so in a cluster stretched across zones clients prefer a ready member in their own zone. Removing the annotation clears the field; an invalid value is reported as a Warning Event and ignored. See [docs/concepts.md](docs/concepts.md#client-service-traffic-distribution). - **Monitoring / autoscaling hooks**: every member Pod always exposes a plaintext `metrics` container port at `2381` (etcd's `/health` + Prometheus `/metrics`) for `VMPodScrape` / `PodMonitor`. The `EtcdCluster` CRD exposes the `/scale` subresource with a populated `status.selector`, making it a valid target for `kubectl scale` and `VerticalPodAutoscaler.targetRef`. - **Locking pattern**: `status.observed` snapshots the in-flight target so mid-flight spec edits don't corrupt consensus; `progressDeadline` bounds how long the operator will spend trying to reach a target. diff --git a/api/v1alpha2/etcdcluster_types.go b/api/v1alpha2/etcdcluster_types.go index 8bdd5253..2aefe36c 100644 --- a/api/v1alpha2/etcdcluster_types.go +++ b/api/v1alpha2/etcdcluster_types.go @@ -548,6 +548,16 @@ type EtcdClusterSpec struct { // +optional TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Tolerations lets member Pods schedule onto tainted nodes (for example + // a dedicated platform node pool). Passed straight through to each + // member Pod's spec.tolerations. Tolerations only permit scheduling onto + // tainted nodes; pair them with affinity to steer members there. + // + // Updates take effect on newly-created members (scale-up, replacement); + // the operator does not roll existing Pods to apply a change in place. + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + // Options carries etcd server tuning flags (backend quota, // auto-compaction, raft snapshot count) passed to each member's // command line. A closed typed set — see EtcdOptions for why there @@ -622,6 +632,11 @@ type ObservedClusterSpec struct { // +optional TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Tolerations is the locked target tolerations for member Pods. + // Latched with the rest of the target spec. + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + // AdditionalMetadata is the locked target extra labels/annotations // stamped onto objects created for this cluster. Latched with the rest // of the target spec so a mid-flight metadata edit only applies to diff --git a/api/v1alpha2/etcdmember_types.go b/api/v1alpha2/etcdmember_types.go index 46599fd4..dbba188c 100644 --- a/api/v1alpha2/etcdmember_types.go +++ b/api/v1alpha2/etcdmember_types.go @@ -115,6 +115,12 @@ type EtcdMemberSpec struct { // +optional TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` + // Tolerations mirrors EtcdCluster.spec.tolerations at the time this + // member was created. Passed straight to the Pod's spec.tolerations at + // build time. + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty"` + // Options mirrors EtcdCluster.spec.options at the time this member // was created. The member controller renders the set fields as etcd // command-line flags at Pod-build time; existing members are not diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index 7f59359e..af92b584 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -292,6 +292,13 @@ func (in *EtcdClusterSpec) DeepCopyInto(out *EtcdClusterSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Options != nil { in, out := &in.Options, &out.Options *out = new(EtcdOptions) @@ -709,6 +716,13 @@ func (in *EtcdMemberSpec) DeepCopyInto(out *EtcdMemberSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Options != nil { in, out := &in.Options, &out.Options *out = new(EtcdOptions) @@ -967,6 +981,13 @@ func (in *ObservedClusterSpec) DeepCopyInto(out *ObservedClusterSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.AdditionalMetadata != nil { in, out := &in.AdditionalMetadata, &out.AdditionalMetadata *out = new(AdditionalMetadata) diff --git a/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml b/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml index ed4d78ad..2d6418f5 100644 --- a/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml +++ b/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdclusters.yaml @@ -1602,6 +1602,52 @@ spec: must be set rule: has(self.secretRef) != has(self.certManager) type: object + tolerations: + description: |- + Tolerations lets member Pods schedule onto tainted nodes (for example + a dedicated platform node pool). Passed straight through to each + member Pod's spec.tolerations. Tolerations only permit scheduling onto + tainted nodes; pair them with affinity to steer members there. + + Updates take effect on newly-created members (scale-up, replacement); + the operator does not roll existing Pods to apply a change in place. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array topologySpreadConstraints: description: |- TopologySpreadConstraints controls how member Pods are spread across @@ -3083,6 +3129,47 @@ spec: the field is being added from nil. type: string type: object + tolerations: + description: |- + Tolerations is the locked target tolerations for member Pods. + Latched with the rest of the target spec. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array topologySpreadConstraints: description: |- TopologySpreadConstraints is the locked target spread configuration diff --git a/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdmembers.yaml b/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdmembers.yaml index fbad5192..1bc6186b 100644 --- a/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdmembers.yaml +++ b/charts/etcd-operator/crd-bases/etcd-operator.cozystack.io_etcdmembers.yaml @@ -1389,6 +1389,48 @@ spec: type: object x-kubernetes-map-type: atomic type: object + tolerations: + description: |- + Tolerations mirrors EtcdCluster.spec.tolerations at the time this + member was created. Passed straight to the Pod's spec.tolerations at + build time. + items: + description: |- + The pod this Toleration is attached to tolerates any taint that matches + the triple using the matching operator . + properties: + effect: + description: |- + Effect indicates the taint effect to match. Empty means match all taint effects. + When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute. + type: string + key: + description: |- + Key is the taint key that the toleration applies to. Empty means match all taint keys. + If the key is empty, operator must be Exists; this combination means to match all values and all keys. + type: string + operator: + description: |- + Operator represents a key's relationship to the value. + Valid operators are Exists and Equal. Defaults to Equal. + Exists is equivalent to wildcard for value, so that a pod can + tolerate all taints of a particular category. + type: string + tolerationSeconds: + description: |- + TolerationSeconds represents the period of time the toleration (which must be + of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, + it is not set, which means tolerate the taint forever (do not evict). Zero and + negative values will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: |- + Value is the taint value the toleration matches to. + If the operator is Exists, the value should be empty, otherwise just a regular string. + type: string + type: object + type: array topologySpreadConstraints: description: |- TopologySpreadConstraints mirrors EtcdCluster.spec.topologySpreadConstraints diff --git a/controllers/etcdcluster_controller.go b/controllers/etcdcluster_controller.go index 49464ae3..99c7a8fa 100644 --- a/controllers/etcdcluster_controller.go +++ b/controllers/etcdcluster_controller.go @@ -449,6 +449,7 @@ func (r *EtcdClusterReconciler) bootstrap( AdditionalMetadata: cluster.Status.Observed.AdditionalMetadata, Affinity: cluster.Status.Observed.Affinity, TopologySpreadConstraints: cluster.Status.Observed.TopologySpreadConstraints, + Tolerations: cluster.Status.Observed.Tolerations, Options: cluster.Status.Observed.Options, ImagePullSecrets: cluster.Status.Observed.ImagePullSecrets, Bootstrap: true, @@ -854,6 +855,7 @@ func (r *EtcdClusterReconciler) scaleUp( AdditionalMetadata: cluster.Status.Observed.AdditionalMetadata, Affinity: cluster.Status.Observed.Affinity, TopologySpreadConstraints: cluster.Status.Observed.TopologySpreadConstraints, + Tolerations: cluster.Status.Observed.Tolerations, Options: cluster.Status.Observed.Options, ImagePullSecrets: cluster.Status.Observed.ImagePullSecrets, Bootstrap: false, @@ -2138,6 +2140,7 @@ func snapshotSpecIntoObserved(cluster *lll.EtcdCluster) { Resources: cluster.Spec.Resources, Affinity: cluster.Spec.Affinity, TopologySpreadConstraints: cluster.Spec.TopologySpreadConstraints, + Tolerations: cluster.Spec.Tolerations, AdditionalMetadata: cluster.Spec.AdditionalMetadata, Options: cluster.Spec.Options, ImagePullSecrets: cluster.Spec.ImagePullSecrets, @@ -2160,6 +2163,7 @@ func specEqualsObserved(cluster *lll.EtcdCluster) bool { equality.Semantic.DeepEqual(o.Resources, cluster.Spec.Resources) && equality.Semantic.DeepEqual(o.Affinity, cluster.Spec.Affinity) && equality.Semantic.DeepEqual(o.TopologySpreadConstraints, cluster.Spec.TopologySpreadConstraints) && + equality.Semantic.DeepEqual(o.Tolerations, cluster.Spec.Tolerations) && equality.Semantic.DeepEqual(o.AdditionalMetadata, cluster.Spec.AdditionalMetadata) && equality.Semantic.DeepEqual(o.Options, cluster.Spec.Options) && equality.Semantic.DeepEqual(o.ImagePullSecrets, cluster.Spec.ImagePullSecrets) diff --git a/controllers/etcdcluster_controller_test.go b/controllers/etcdcluster_controller_test.go index 74ac7caa..e94fddab 100644 --- a/controllers/etcdcluster_controller_test.go +++ b/controllers/etcdcluster_controller_test.go @@ -3340,6 +3340,12 @@ func TestBootstrap_PropagatesSchedulingAndMetadataToSeed(t *testing.T) { TopologyKey: "topology.kubernetes.io/zone", WhenUnsatisfiable: corev1.DoNotSchedule, }} + tols := []corev1.Toleration{{ + Key: "feature.node.kubernetes.io/dedicated-node", + Operator: corev1.TolerationOpEqual, + Value: "platform", + Effect: corev1.TaintEffectNoSchedule, + }} cluster := &lll.EtcdCluster{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "ns"}, Spec: lll.EtcdClusterSpec{ @@ -3348,6 +3354,7 @@ func TestBootstrap_PropagatesSchedulingAndMetadataToSeed(t *testing.T) { Storage: lll.StorageSpec{Size: quickQty(t, "1Gi")}, Affinity: aff, TopologySpreadConstraints: tsc, + Tolerations: tols, AdditionalMetadata: &lll.AdditionalMetadata{ Labels: map[string]string{"cozystack.io/tenant": "foo"}, Annotations: map[string]string{"example.com/note": "bar"}, @@ -3369,6 +3376,9 @@ func TestBootstrap_PropagatesSchedulingAndMetadataToSeed(t *testing.T) { if !equality.Semantic.DeepEqual(got.Status.Observed.TopologySpreadConstraints, tsc) { t.Errorf("Observed.TopologySpreadConstraints = %+v; want %+v", got.Status.Observed.TopologySpreadConstraints, tsc) } + if !equality.Semantic.DeepEqual(got.Status.Observed.Tolerations, tols) { + t.Errorf("Observed.Tolerations = %+v; want %+v", got.Status.Observed.Tolerations, tols) + } if got.Status.Observed.AdditionalMetadata == nil || got.Status.Observed.AdditionalMetadata.Labels["cozystack.io/tenant"] != "foo" { t.Errorf("Observed.AdditionalMetadata not latched: %+v", got.Status.Observed.AdditionalMetadata) @@ -3388,6 +3398,9 @@ func TestBootstrap_PropagatesSchedulingAndMetadataToSeed(t *testing.T) { if !equality.Semantic.DeepEqual(seed.Spec.TopologySpreadConstraints, tsc) { t.Errorf("seed Spec.TopologySpreadConstraints = %+v; want %+v", seed.Spec.TopologySpreadConstraints, tsc) } + if !equality.Semantic.DeepEqual(seed.Spec.Tolerations, tols) { + t.Errorf("seed Spec.Tolerations = %+v; want %+v", seed.Spec.Tolerations, tols) + } if seed.Spec.AdditionalMetadata == nil || seed.Spec.AdditionalMetadata.Labels["cozystack.io/tenant"] != "foo" { t.Errorf("seed Spec.AdditionalMetadata not propagated: %+v", seed.Spec.AdditionalMetadata) } diff --git a/controllers/etcdmember_controller.go b/controllers/etcdmember_controller.go index f3e87112..295085f3 100644 --- a/controllers/etcdmember_controller.go +++ b/controllers/etcdmember_controller.go @@ -858,6 +858,7 @@ func (r *EtcdMemberReconciler) buildPod(member *lll.EtcdMember, clusterFormed bo Subdomain: memberServiceName(member), Affinity: member.Spec.Affinity, TopologySpreadConstraints: member.Spec.TopologySpreadConstraints, + Tolerations: member.Spec.Tolerations, ImagePullSecrets: member.Spec.ImagePullSecrets, InitContainers: initContainers, // etcd and the restore agent never call the Kubernetes API, so diff --git a/controllers/etcdmember_controller_test.go b/controllers/etcdmember_controller_test.go index 41d35bcb..f156f7b0 100644 --- a/controllers/etcdmember_controller_test.go +++ b/controllers/etcdmember_controller_test.go @@ -1339,6 +1339,12 @@ func TestBuildPod_AppliesSchedulingAndMetadata(t *testing.T) { TopologyKey: "topology.kubernetes.io/zone", WhenUnsatisfiable: corev1.DoNotSchedule, }} + tols := []corev1.Toleration{{ + Key: "feature.node.kubernetes.io/dedicated-node", + Operator: corev1.TolerationOpEqual, + Value: "platform", + Effect: corev1.TaintEffectNoSchedule, + }} pod := r.buildPod(&lll.EtcdMember{ ObjectMeta: metav1.ObjectMeta{Name: "test-0", Namespace: "ns"}, Spec: lll.EtcdMemberSpec{ @@ -1346,6 +1352,7 @@ func TestBuildPod_AppliesSchedulingAndMetadata(t *testing.T) { Version: "3.5.17", Affinity: aff, TopologySpreadConstraints: tsc, + Tolerations: tols, AdditionalMetadata: &lll.AdditionalMetadata{ Labels: map[string]string{ "cozystack.io/tenant": "foo", @@ -1363,6 +1370,9 @@ func TestBuildPod_AppliesSchedulingAndMetadata(t *testing.T) { if !equality.Semantic.DeepEqual(pod.Spec.TopologySpreadConstraints, tsc) { t.Errorf("pod topologySpreadConstraints = %+v, want %+v", pod.Spec.TopologySpreadConstraints, tsc) } + if !equality.Semantic.DeepEqual(pod.Spec.Tolerations, tols) { + t.Errorf("pod tolerations = %+v, want %+v", pod.Spec.Tolerations, tols) + } if got := pod.Labels["cozystack.io/tenant"]; got != "foo" { t.Errorf("additional label not merged: cozystack.io/tenant = %q, want foo", got) } diff --git a/docs/concepts.md b/docs/concepts.md index 54eefabc..bb2c0a98 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -252,7 +252,7 @@ These rules live in the CRD itself; the apiserver enforces them with no separate ## Pod scheduling and additional metadata -Three spec fields shape where member Pods land and what metadata the operator's child objects carry. All three are latched through `status.observed` like the rest of the target spec (see [Locking pattern](#locking-pattern)) and apply **at object creation only** — the operator does not roll existing Pods or re-stamp existing objects when they change. +Four spec fields shape where member Pods land and what metadata the operator's child objects carry. All four are latched through `status.observed` like the rest of the target spec (see [Locking pattern](#locking-pattern)) and apply **at object creation only** — the operator does not roll existing Pods or re-stamp existing objects when they change. ### `spec.affinity` and `spec.topologySpreadConstraints` @@ -271,6 +271,21 @@ spec: Changes take effect on newly-created members (scale-up, replacement). To apply a scheduling change to an existing cluster, delete one Pod at a time and let the operator recreate it with the new constraints. +### `spec.tolerations` + +Passed through verbatim to each member Pod's `spec.tolerations`, so members can run on tainted nodes (for example a dedicated platform node pool). Tolerations only *permit* scheduling onto a tainted node; pair them with `spec.affinity` to steer members there: + +```yaml +spec: + tolerations: + - key: dedicated + operator: Equal + value: platform + effect: NoSchedule +``` + +As with the other scheduling fields, a change applies to newly-created members (scale-up, replacement); existing Pods are not rolled. + ### `spec.additionalMetadata` Extra labels and annotations the operator merges onto **every object it creates** for the cluster: member Pods, the per-member data PVCs (`data-`), the client and headless Services, the PodDisruptionBudget, and the `EtcdMember` CRs. Typical uses are backup-tool selectors on the PVCs and cost-allocation/tenancy labels across the board. diff --git a/internal/migrate/adopt.go b/internal/migrate/adopt.go index 7410ecee..eca2bccd 100644 --- a/internal/migrate/adopt.go +++ b/internal/migrate/adopt.go @@ -278,6 +278,7 @@ func BuildAdoption(name, namespace string, spec legacy.EtcdClusterSpec, facts Cl AdditionalMetadata: cluster.Spec.AdditionalMetadata, Affinity: cluster.Spec.Affinity, TopologySpreadConstraints: cluster.Spec.TopologySpreadConstraints, + Tolerations: cluster.Spec.Tolerations, Options: cluster.Spec.Options, ImagePullSecrets: cluster.Spec.ImagePullSecrets, Bootstrap: false, @@ -310,6 +311,7 @@ func BuildAdoption(name, namespace string, spec legacy.EtcdClusterSpec, facts Cl Resources: cluster.Spec.Resources, Affinity: cluster.Spec.Affinity, TopologySpreadConstraints: cluster.Spec.TopologySpreadConstraints, + Tolerations: cluster.Spec.Tolerations, AdditionalMetadata: cluster.Spec.AdditionalMetadata, Options: cluster.Spec.Options, ImagePullSecrets: cluster.Spec.ImagePullSecrets, diff --git a/internal/migrate/translate.go b/internal/migrate/translate.go index 0a361bff..f2fcc4db 100644 --- a/internal/migrate/translate.go +++ b/internal/migrate/translate.go @@ -289,6 +289,7 @@ func translatePodTemplate(pt legacy.PodTemplate, out *lll.EtcdCluster, plan *Res ps := pt.Spec out.Spec.Affinity = ps.Affinity out.Spec.TopologySpreadConstraints = ps.TopologySpreadConstraints + out.Spec.Tolerations = ps.Tolerations // Carry pull secrets so the new operator can still pull from a private // (e.g. air-gapped) registry. v1alpha2 grew spec.imagePullSecrets, so @@ -332,7 +333,6 @@ func translatePodTemplate(pt legacy.PodTemplate, out *lll.EtcdCluster, plan *Res "initContainers": len(ps.InitContainers) > 0, "volumes": len(ps.Volumes) > 0, "nodeSelector": len(ps.NodeSelector) > 0, - "tolerations": len(ps.Tolerations) > 0, "serviceAccountName": ps.ServiceAccountName != "", "securityContext": ps.SecurityContext != nil && !equality.Semantic.DeepEqual(*ps.SecurityContext, corev1.PodSecurityContext{}), "priorityClassName": ps.PriorityClassName != "", diff --git a/internal/migrate/translate_test.go b/internal/migrate/translate_test.go index 68af67a0..0f0f8733 100644 --- a/internal/migrate/translate_test.go +++ b/internal/migrate/translate_test.go @@ -64,6 +64,7 @@ func TestTranslateCluster_KitchenSink(t *testing.T) { RequiredDuringSchedulingIgnoredDuringExecution: []corev1.PodAffinityTerm{{TopologyKey: "kubernetes.io/hostname"}}, }} tsc := []corev1.TopologySpreadConstraint{{MaxSkew: 1, TopologyKey: "zone", WhenUnsatisfiable: corev1.DoNotSchedule}} + tols := []corev1.Toleration{{Key: "dedicated", Operator: corev1.TolerationOpEqual, Value: "platform", Effect: corev1.TaintEffectNoSchedule}} res := corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceCPU: qty(t, "500m")}} spec := legacy.EtcdClusterSpec{ @@ -85,6 +86,7 @@ func TestTranslateCluster_KitchenSink(t *testing.T) { Spec: corev1.PodSpec{ Affinity: aff, TopologySpreadConstraints: tsc, + Tolerations: tols, NodeSelector: map[string]string{"disk": "ssd"}, Containers: []corev1.Container{ {Name: "etcd", Image: "quay.io/coreos/etcd:v3.5.21", Resources: res, @@ -145,6 +147,12 @@ func TestTranslateCluster_KitchenSink(t *testing.T) { if !equality.Semantic.DeepEqual(out.Spec.TopologySpreadConstraints, tsc) { t.Errorf("topologySpreadConstraints not mapped: %+v", out.Spec.TopologySpreadConstraints) } + if !equality.Semantic.DeepEqual(out.Spec.Tolerations, tols) { + t.Errorf("tolerations not mapped: %+v", out.Spec.Tolerations) + } + if hasWarning(plan.Warnings, "tolerations") { + t.Errorf("tolerations must no longer be dropped; warnings: %v", plan.Warnings) + } if !equality.Semantic.DeepEqual(out.Spec.Resources, res) { t.Errorf("resources not mapped: %+v", out.Spec.Resources) }