Repository: k8ssandra/k8ssandra-operator (go)

Issue:
K8SSAND-1239 ⁃ Delete cass-operator namespace from kustomize deployments
While we now have all k8ssandra-operator components installed into the k8ssandra-operator namespace, it appears that the cass-operator namespace is still being created (because it exists independently in the cass-operator kustomizations upstream).

Since this namespace is now unused we should arrange to delete it in the {{single-ns}} component.

┆Issue is synchronized with this [Jira Task](https://k8ssandra.atlassian.net/browse/K8SSAND-1239) by [Unito](https://www.unito.io)
┆Reviewer: 5fa4593062584c006b1e04d0
┆fixVersions: k8ssandra-operator-v1.0.0
┆friendlyId: K8SSAND-1239
┆priority: Medium


Pull request:
Remove cass-operator namespace [K8SSAND-1239]
**What this PR does**:

All components now install into the namespace k8ssandra-operator. But the cass-operator namespace is still created. 

This PR adds a patch to delete it.

**Which issue(s) this PR fixes**:
Fixes #385 

**Checklist**
- [ ] Changes manually tested
- [ ] Automated Tests added/updated
- [ ] Documentation added/updated
- [ ] CHANGELOG.md updated (not required for documentation PRs)
- [ ] CLA Signed:  [DataStax CLA](https://cla.datastax.com/)


Patch:
diff --git a/CHANGELOG/CHANGELOG-1.0.md b/CHANGELOG/CHANGELOG-1.0.md
index 70728d59..ca8a0381 100644
--- a/CHANGELOG/CHANGELOG-1.0.md
+++ b/CHANGELOG/CHANGELOG-1.0.md
@@ -15,9 +15,17 @@ When cutting a new release, update the `unreleased` heading to the tag being gen
 
 ## unreleased
 
+* [FEATURE] [#402](https://github.com/k8ssandra/k8ssandra-operator/issues/402) Expose CassandraDatacenter Tolerations property
+* [BUGFIX] [#381](https://github.com/k8ssandra/k8ssandra-operator/issues/381) Add additionalSeeds property to support migrations
+* [TESTING] [#332](https://github.com/k8ssandra/k8ssandra-operator/issues/332) Configure mocks to avoid panics
+
+## v0.5.0 2022-02-10
+
+* [CHANGE] [#120](https://github.com/k8ssandra/k8ssandra-operator/issues/120) Remove `systemLoggerResources` from K8ssandraCluster spec
 * [FEATURE] [#296](https://github.com/k8ssandra/k8ssandra-operator/issues/296) Expose the stopped property from the
   CassandraDatacenter spec
 * [BUGFIX] [#389](https://github.com/k8ssandra/k8ssandra-operator/issues/389) Configure watches for control plane client
+* [BUGFIX] [#398](https://github.com/k8ssandra/k8ssandra-operator/issues/398) Removing a DC with size > 1 causes panic
 * [TESTING] [#332](https://github.com/k8ssandra/k8ssandra-operator/issues/332) Configure mocks to avoid panics
 * [TESTING] [#361](https://github.com/k8ssandra/k8ssandra-operator/issues/361) Reduce flakiness in Stargate API tests
 
diff --git a/apis/config/v1beta1/zz_generated.deepcopy.go b/apis/config/v1beta1/zz_generated.deepcopy.go
index 40a35a45..f0bc5e66 100644
--- a/apis/config/v1beta1/zz_generated.deepcopy.go
+++ b/apis/config/v1beta1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/apis/k8ssandra/v1alpha1/k8ssandracluster_types.go b/apis/k8ssandra/v1alpha1/k8ssandracluster_types.go
index 19354582..7a7320ec 100644
--- a/apis/k8ssandra/v1alpha1/k8ssandracluster_types.go
+++ b/apis/k8ssandra/v1alpha1/k8ssandracluster_types.go
@@ -245,11 +245,24 @@ type CassandraClusterTemplate struct {
 	// +optional
 	MgmtAPIHeap *resource.Quantity `json:"mgmtAPIHeap,omitempty"`
 
+	// AdditionalSeeds specifies Cassandra node IPs for an existing datacenter. This is
+	// primarily intended for migrations from an existing Cassandra cluster that is not
+	// managed by k8ssandra-operator. Note that this property should NOT be used to set
+	// seeds for a DC that is or will be managed by k8ssandra-operator. k8ssandra-operator
+	// already manages seeds for DCs that it manages. If you have DNS set up such that you
+	// can resolve hostnames for the remote Cassandra cluster, then you can specify hostnames
+	// here; otherwise, use IP addresses.
+	AdditionalSeeds []string `json:"additionalSeeds,omitempty"`
+
 	// SoftPodAntiAffinity sets whether multiple Cassandra instances can be scheduled on the same node.
 	// This should normally be false to ensure cluster resilience but may be set true for test/dev scenarios to minimise
 	// the number of nodes required.
 	SoftPodAntiAffinity *bool `json:"softPodAntiAffinity,omitempty"`
 
+	// Tolerations applied to every Cassandra pod.
+	// +optional
+	Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
+
 	// Internode encryption stores which are used by Cassandra and Stargate.
 	// +optional
 	ServerEncryptionStores *encryption.Stores `json:"serverEncryptionStores,omitempty"`
@@ -334,6 +347,10 @@ type CassandraDatacenterTemplate struct {
 	// This should normally be false to ensure cluster resilience but may be set true for test/dev scenarios to minimise
 	// the number of nodes required.
 	SoftPodAntiAffinity *bool `json:"softPodAntiAffinity,omitempty"`
+
+	// Tolerations applied to every Cassandra pod.
+	// +optional
+	Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
 }
 
 type EmbeddedObjectMeta struct {
diff --git a/apis/k8ssandra/v1alpha1/zz_generated.deepcopy.go b/apis/k8ssandra/v1alpha1/zz_generated.deepcopy.go
index e51d3490..21482c2d 100644
--- a/apis/k8ssandra/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/k8ssandra/v1alpha1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
@@ -155,11 +156,23 @@ func (in *CassandraClusterTemplate) DeepCopyInto(out *CassandraClusterTemplate)
 		x := (*in).DeepCopy()
 		*out = &x
 	}
+	if in.AdditionalSeeds != nil {
+		in, out := &in.AdditionalSeeds, &out.AdditionalSeeds
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
 	if in.SoftPodAntiAffinity != nil {
 		in, out := &in.SoftPodAntiAffinity, &out.SoftPodAntiAffinity
 		*out = new(bool)
 		**out = **in
 	}
+	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.ServerEncryptionStores != nil {
 		in, out := &in.ServerEncryptionStores, &out.ServerEncryptionStores
 		*out = new(encryption.Stores)
@@ -255,6 +268,13 @@ func (in *CassandraDatacenterTemplate) DeepCopyInto(out *CassandraDatacenterTemp
 		*out = new(bool)
 		**out = **in
 	}
+	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])
+		}
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CassandraDatacenterTemplate.
diff --git a/apis/medusa/v1alpha1/zz_generated.deepcopy.go b/apis/medusa/v1alpha1/zz_generated.deepcopy.go
index a6581894..25b015dc 100644
--- a/apis/medusa/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/medusa/v1alpha1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/apis/reaper/v1alpha1/zz_generated.deepcopy.go b/apis/reaper/v1alpha1/zz_generated.deepcopy.go
index 5bc7a83f..4b243db5 100644
--- a/apis/reaper/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/reaper/v1alpha1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/apis/replication/v1alpha1/zz_generated.deepcopy.go b/apis/replication/v1alpha1/zz_generated.deepcopy.go
index a397bc19..710fcead 100644
--- a/apis/replication/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/replication/v1alpha1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/apis/stargate/v1alpha1/zz_generated.deepcopy.go b/apis/stargate/v1alpha1/zz_generated.deepcopy.go
index 0b2ceb83..aaf636d2 100644
--- a/apis/stargate/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/stargate/v1alpha1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/apis/telemetry/v1alpha1/zz_generated.deepcopy.go b/apis/telemetry/v1alpha1/zz_generated.deepcopy.go
index 7be31b4a..6eb70e8d 100644
--- a/apis/telemetry/v1alpha1/zz_generated.deepcopy.go
+++ b/apis/telemetry/v1alpha1/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/config/components/single-namespace/kustomization.yaml b/config/components/single-namespace/kustomization.yaml
index 9b6fb88a..cbded00a 100644
--- a/config/components/single-namespace/kustomization.yaml
+++ b/config/components/single-namespace/kustomization.yaml
@@ -22,5 +22,11 @@ replacements:
         kind: ValidatingWebhookConfiguration
       fieldPaths:
       - webhooks.0.clientConfig.service.namespace
-  
+patchesStrategicMerge:
+- |-
+  apiVersion: v1
+  kind: Namespace
+  metadata:
+    name: cass-operator
+  $patch: delete
     
\ No newline at end of file
diff --git a/config/crd/bases/config.k8ssandra.io_clientconfigs.yaml b/config/crd/bases/config.k8ssandra.io_clientconfigs.yaml
index fa42c300..26b682c6 100644
--- a/config/crd/bases/config.k8ssandra.io_clientconfigs.yaml
+++ b/config/crd/bases/config.k8ssandra.io_clientconfigs.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: clientconfigs.config.k8ssandra.io
 spec:
diff --git a/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml b/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml
index 365f7bb3..f4b8bf37 100644
--- a/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml
+++ b/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: k8ssandraclusters.k8ssandra.io
 spec:
@@ -55,6 +55,19 @@ spec:
                   cluster where each DC should be deployed, node affinity (via racks),
                   individual C* node settings, JVM settings, and more.
                 properties:
+                  additionalSeeds:
+                    description: AdditionalSeeds specifies Cassandra node IPs for
+                      an existing datacenter. This is primarily intended for migrations
+                      from an existing Cassandra cluster that is not managed by k8ssandra-operator.
+                      Note that this property should NOT be used to set seeds for
+                      a DC that is or will be managed by k8ssandra-operator. k8ssandra-operator
+                      already manages seeds for DCs that it manages. If you have DNS
+                      set up such that you can resolve hostnames for the remote Cassandra
+                      cluster, then you can specify hostnames here; otherwise, use
+                      IP addresses.
+                    items:
+                      type: string
+                    type: array
                   cassandraTelemetry:
                     description: CassandraTelemetry defines the desired state for
                       telemetry resources in this K8ssandraCluster. If telemetry configurations
@@ -5853,6 +5866,49 @@ spec:
                                   type: string
                               type: object
                           type: object
+                        tolerations:
+                          description: Tolerations applied to every Cassandra pod.
+                          items:
+                            description: The pod this Toleration is attached to tolerates
+                              any taint that matches the triple <key,value,effect>
+                              using the matching operator <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
                       required:
                       - size
                       type: object
@@ -6399,6 +6455,47 @@ spec:
                           TODO: Add other useful fields. apiVersion, kind, uid?'
                         type: string
                     type: object
+                  tolerations:
+                    description: Tolerations applied to every Cassandra pod.
+                    items:
+                      description: The pod this Toleration is attached to tolerates
+                        any taint that matches the triple <key,value,effect> using
+                        the matching operator <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
                 type: object
               medusa:
                 description: Medusa defines the desired deployment characteristics
diff --git a/config/crd/bases/medusa.k8ssandra.io_cassandrabackups.yaml b/config/crd/bases/medusa.k8ssandra.io_cassandrabackups.yaml
index d08a6e5a..fdd61faa 100644
--- a/config/crd/bases/medusa.k8ssandra.io_cassandrabackups.yaml
+++ b/config/crd/bases/medusa.k8ssandra.io_cassandrabackups.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: cassandrabackups.medusa.k8ssandra.io
 spec:
diff --git a/config/crd/bases/medusa.k8ssandra.io_cassandrarestores.yaml b/config/crd/bases/medusa.k8ssandra.io_cassandrarestores.yaml
index 6ff0bbe9..acd59a78 100644
--- a/config/crd/bases/medusa.k8ssandra.io_cassandrarestores.yaml
+++ b/config/crd/bases/medusa.k8ssandra.io_cassandrarestores.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: cassandrarestores.medusa.k8ssandra.io
 spec:
diff --git a/config/crd/bases/reaper.k8ssandra.io_reapers.yaml b/config/crd/bases/reaper.k8ssandra.io_reapers.yaml
index 6a747c42..d3d70f26 100644
--- a/config/crd/bases/reaper.k8ssandra.io_reapers.yaml
+++ b/config/crd/bases/reaper.k8ssandra.io_reapers.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: reapers.reaper.k8ssandra.io
 spec:
diff --git a/config/crd/bases/replication.k8ssandra.io_replicatedsecrets.yaml b/config/crd/bases/replication.k8ssandra.io_replicatedsecrets.yaml
index f5c4c51b..f2b021de 100644
--- a/config/crd/bases/replication.k8ssandra.io_replicatedsecrets.yaml
+++ b/config/crd/bases/replication.k8ssandra.io_replicatedsecrets.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: replicatedsecrets.replication.k8ssandra.io
 spec:
diff --git a/config/crd/bases/stargate.k8ssandra.io_stargates.yaml b/config/crd/bases/stargate.k8ssandra.io_stargates.yaml
index bb130e77..eb3a9149 100644
--- a/config/crd/bases/stargate.k8ssandra.io_stargates.yaml
+++ b/config/crd/bases/stargate.k8ssandra.io_stargates.yaml
@@ -4,7 +4,7 @@ apiVersion: apiextensions.k8s.io/v1
 kind: CustomResourceDefinition
 metadata:
   annotations:
-    controller-gen.kubebuilder.io/version: v0.6.1
+    controller-gen.kubebuilder.io/version: v0.7.0
   creationTimestamp: null
   name: stargates.stargate.k8ssandra.io
 spec:
diff --git a/controllers/k8ssandra/cleanup.go b/controllers/k8ssandra/cleanup.go
index e445f840..f4cdaa54 100644
--- a/controllers/k8ssandra/cleanup.go
+++ b/controllers/k8ssandra/cleanup.go
@@ -198,7 +198,7 @@ func (r *K8ssandraClusterReconciler) deleteDc(ctx context.Context, kc *api.K8ssa
 		if err = remoteClient.Delete(ctx, dc); err != nil && !errors.IsNotFound(err) {
 			return result.Error(fmt.Errorf("failed to delete CassandraDatacenter (%s): %v", dcName, err))
 		}
-		logger.Info("Deleted CassandraDatacenter", utils.GetKey(dc))
+		logger.Info("Deleted CassandraDatacenter", "CassandraDatacenter", utils.GetKey(dc))
 		// There is no need to requeue here. Reconciliation will be trigger by updates made by cass-operator.
 		return result.Done()
 	}
diff --git a/controllers/k8ssandra/datacenters.go b/controllers/k8ssandra/datacenters.go
index f0a8d1be..37a83d71 100644
--- a/controllers/k8ssandra/datacenters.go
+++ b/controllers/k8ssandra/datacenters.go
@@ -105,7 +105,7 @@ func (r *K8ssandraClusterReconciler) reconcileDatacenters(ctx context.Context, k
 
 		actualDc := &cassdcapi.CassandraDatacenter{}
 
-		if recResult := r.reconcileSeedsEndpoints(ctx, desiredDc, seeds, remoteClient, logger); recResult.Completed() {
+		if recResult := r.reconcileSeedsEndpoints(ctx, desiredDc, seeds, dcConfig.AdditionalSeeds, remoteClient, logger); recResult.Completed() {
 			return recResult, actualDcs
 		}
 
diff --git a/controllers/k8ssandra/seeds.go b/controllers/k8ssandra/seeds.go
index fcc5bf3e..ce83742d 100644
--- a/controllers/k8ssandra/seeds.go
+++ b/controllers/k8ssandra/seeds.go
@@ -50,39 +50,34 @@ func (r *K8ssandraClusterReconciler) findSeeds(ctx context.Context, kc *api.K8ss
 	return pods, nil
 }
 
-func (r *K8ssandraClusterReconciler) reconcileSeedsEndpoints(ctx context.Context, dc *cassdcapi.CassandraDatacenter, seeds []corev1.Pod, remoteClient client.Client, logger logr.Logger) result.ReconcileResult {
+func (r *K8ssandraClusterReconciler) reconcileSeedsEndpoints(
+	ctx context.Context,
+	dc *cassdcapi.CassandraDatacenter,
+	seeds []corev1.Pod,
+	additionalSeeds []string,
+	remoteClient client.Client,
+	logger logr.Logger) result.ReconcileResult {
 	logger.Info("Reconciling seeds")
 
 	// The following if block was basically taken straight out of cass-operator. See
 	// https://github.com/k8ssandra/k8ssandra-operator/issues/210 for a detailed
 	// explanation of why this is being done.
 
-	desiredEndpoints := newEndpoints(dc, seeds)
+	desiredEndpoints := newEndpoints(dc, seeds, additionalSeeds)
 	actualEndpoints := &corev1.Endpoints{}
 	endpointsKey := client.ObjectKey{Namespace: desiredEndpoints.Namespace, Name: desiredEndpoints.Name}
 
 	if err := remoteClient.Get(ctx, endpointsKey, actualEndpoints); err == nil {
-		// If we already have an Endpoints object but no seeds then it probably means all
-		// Cassandra pods are down or not ready for some reason. In this case we will
-		// delete the Endpoints and let it get recreated once we have seed nodes.
-		if len(seeds) == 0 {
-			logger.Info("Deleting endpoints")
-			if err := remoteClient.Delete(ctx, actualEndpoints); err != nil {
-				logger.Error(err, "Failed to delete endpoints")
+		if !annotations.CompareHashAnnotations(actualEndpoints, desiredEndpoints) {
+			logger.Info("Updating endpoints", "Endpoints", endpointsKey)
+			actualEndpoints := actualEndpoints.DeepCopy()
+			resourceVersion := actualEndpoints.GetResourceVersion()
+			desiredEndpoints.DeepCopyInto(actualEndpoints)
+			actualEndpoints.SetResourceVersion(resourceVersion)
+			if err = remoteClient.Update(ctx, actualEndpoints); err != nil {
+				logger.Error(err, "Failed to update endpoints", "Endpoints", endpointsKey)
 				return result.Error(err)
 			}
-		} else {
-			if !annotations.CompareHashAnnotations(actualEndpoints, desiredEndpoints) {
-				logger.Info("Updating endpoints", "Endpoints", endpointsKey)
-				actualEndpoints := actualEndpoints.DeepCopy()
-				resourceVersion := actualEndpoints.GetResourceVersion()
-				desiredEndpoints.DeepCopyInto(actualEndpoints)
-				actualEndpoints.SetResourceVersion(resourceVersion)
-				if err = remoteClient.Update(ctx, actualEndpoints); err != nil {
-					logger.Error(err, "Failed to update endpoints", "Endpoints", endpointsKey)
-					return result.Error(err)
-				}
-			}
 		}
 	} else {
 		if errors.IsNotFound(err) {
@@ -93,7 +88,7 @@ func (r *K8ssandraClusterReconciler) reconcileSeedsEndpoints(ctx context.Context
 			// first created and no pods have reached the ready state. Secondly, you
 			// cannot create an Endpoints object that has both empty Addresses and
 			// empty NotReadyAddresses.
-			if len(seeds) > 0 {
+			if len(seeds) > 0 || len(additionalSeeds) > 0 {
 				logger.Info("Creating endpoints", "Endpoints", endpointsKey)
 				if err = remoteClient.Create(ctx, desiredEndpoints); err != nil {
 					logger.Error(err, "Failed to create endpoints", "Endpoints", endpointsKey)
@@ -110,7 +105,7 @@ func (r *K8ssandraClusterReconciler) reconcileSeedsEndpoints(ctx context.Context
 
 // newEndpoints returns an Endpoints object who is named after the additional seeds service
 // of dc.
-func newEndpoints(dc *cassdcapi.CassandraDatacenter, seeds []corev1.Pod) *corev1.Endpoints {
+func newEndpoints(dc *cassdcapi.CassandraDatacenter, seeds []corev1.Pod, additionalSeeds []string) *corev1.Endpoints {
 	ep := &corev1.Endpoints{
 		ObjectMeta: metav1.ObjectMeta{
 			Namespace:   dc.Namespace,
@@ -127,6 +122,10 @@ func newEndpoints(dc *cassdcapi.CassandraDatacenter, seeds []corev1.Pod) *corev1
 		})
 	}
 
+	for _, seed := range additionalSeeds {
+		addresses = append(addresses, corev1.EndpointAddress{IP: seed})
+	}
+
 	ep.Subsets = []corev1.EndpointSubset{
 		{
 			Addresses: addresses,
diff --git a/go.mod b/go.mod
index 3265c124..e7b28874 100644
--- a/go.mod
+++ b/go.mod
@@ -12,6 +12,7 @@ require (
 	github.com/cespare/xxhash/v2 v2.1.2 // indirect
 	github.com/datastax/go-cassandra-native-protocol v0.0.0-20210829124742-a80a54434112
 	github.com/go-logr/logr v0.4.0
+	github.com/go-logr/zapr v0.4.0 // indirect
 	github.com/go-sql-driver/mysql v1.5.0 // indirect
 	github.com/golang/snappy v0.0.4 // indirect
 	github.com/google/uuid v1.2.0
@@ -25,6 +26,7 @@ require (
 	github.com/sirupsen/logrus v1.8.1
 	github.com/stretchr/testify v1.7.0
 	go.uber.org/atomic v1.9.0 // indirect
+	go.uber.org/zap v1.19.0 // indirect
 	golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e // indirect
 	golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect
 	golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
diff --git a/pkg/cassandra/datacenter.go b/pkg/cassandra/datacenter.go
index af6b49f4..37b38bbe 100644
--- a/pkg/cassandra/datacenter.go
+++ b/pkg/cassandra/datacenter.go
@@ -108,6 +108,7 @@ type DatacenterConfig struct {
 	PodTemplateSpec          *corev1.PodTemplateSpec
 	MgmtAPIHeap              *resource.Quantity
 	SoftPodAntiAffinity      *bool
+	Tolerations              []corev1.Toleration
 	ServerEncryptionStores   *encryption.Stores
 	ClientEncryptionStores   *encryption.Stores
 	ClientKeystorePassword   string
@@ -167,7 +168,6 @@ func NewDatacenter(klusterKey types.NamespacedName, template *DatacenterConfig)
 			SuperuserSecretName: template.SuperuserSecretRef.Name,
 			Users:               template.Users,
 			Networking:          template.Networking,
-			AdditionalSeeds:     template.AdditionalSeeds,
 			PodTemplateSpec:     template.PodTemplateSpec,
 		},
 	}
@@ -184,6 +184,8 @@ func NewDatacenter(klusterKey types.NamespacedName, template *DatacenterConfig)
 		dc.Spec.AllowMultipleNodesPerWorker = *template.SoftPodAntiAffinity
 	}
 
+	dc.Spec.Tolerations = template.Tolerations
+
 	return dc, nil
 }
 
@@ -331,9 +333,17 @@ func Coalesce(clusterName string, clusterTemplate *api.CassandraClusterTemplate,
 	} else {
 		dcConfig.SoftPodAntiAffinity = dcTemplate.SoftPodAntiAffinity
 	}
+
+	if len(dcTemplate.Tolerations) == 0 {
+		dcConfig.Tolerations = clusterTemplate.Tolerations
+	} else {
+		dcConfig.Tolerations = dcTemplate.Tolerations
+	}
+
 	// Client/Server Encryption stores are only defined at the cluster level
 	dcConfig.ServerEncryptionStores = clusterTemplate.ServerEncryptionStores
 	dcConfig.ClientEncryptionStores = clusterTemplate.ClientEncryptionStores
+	dcConfig.AdditionalSeeds = clusterTemplate.AdditionalSeeds
 
 	return dcConfig
 }
diff --git a/pkg/encryption/zz_generated.deepcopy.go b/pkg/encryption/zz_generated.deepcopy.go
index cb6a0871..db165015 100644
--- a/pkg/encryption/zz_generated.deepcopy.go
+++ b/pkg/encryption/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*
diff --git a/pkg/images/zz_generated.deepcopy.go b/pkg/images/zz_generated.deepcopy.go
index 10c3b6c1..ba9c8d29 100644
--- a/pkg/images/zz_generated.deepcopy.go
+++ b/pkg/images/zz_generated.deepcopy.go
@@ -1,3 +1,4 @@
+//go:build !ignore_autogenerated
 // +build !ignore_autogenerated
 
 /*


Test patch:
diff --git a/pkg/cassandra/datacenter_test.go b/pkg/cassandra/datacenter_test.go
index 51738694..533a45ba 100644
--- a/pkg/cassandra/datacenter_test.go
+++ b/pkg/cassandra/datacenter_test.go
@@ -37,6 +37,7 @@ func TestCoalesce(t *testing.T) {
 			clusterName: "k8ssandra",
 			clusterTemplate: &api.CassandraClusterTemplate{
 				SuperuserSecretRef: corev1.LocalObjectReference{Name: "test-superuser"},
+				AdditionalSeeds:    []string{"172.18.0.8", "172.18.0.14"},
 			},
 			dcTemplate: &api.CassandraDatacenterTemplate{
 				Meta: api.EmbeddedObjectMeta{
@@ -59,6 +60,7 @@ func TestCoalesce(t *testing.T) {
 				},
 				SuperuserSecretRef: corev1.LocalObjectReference{Name: "test-superuser"},
 				Size:               3,
+				AdditionalSeeds:    []string{"172.18.0.8", "172.18.0.14"},
 			},
 		},
 		{
@@ -365,6 +367,22 @@ func TestNewDatacenter_AllowMultipleCassPerNodeSet(t *testing.T) {
 	assert.Equal(t, true, dc.Spec.AllowMultipleNodesPerWorker)
 }
 
+func TestNewDatacenter_Tolerations(t *testing.T) {
+	template := GetDatacenterConfig()
+	template.Tolerations = []corev1.Toleration{{
+		Key:      "key1",
+		Operator: corev1.TolerationOpEqual,
+		Value:    "value1",
+		Effect:   corev1.TaintEffectNoSchedule,
+	}}
+	dc, err := NewDatacenter(
+		types.NamespacedName{Name: "testdc", Namespace: "test-namespace"},
+		&template,
+	)
+	assert.NoError(t, err)
+	assert.Equal(t, template.Tolerations, dc.Spec.Tolerations)
+}
+
 // TestNewDatacenter_Fail_NoStorageConfig tests that NewDatacenter fails when no storage config is provided.
 func TestNewDatacenter_Fail_NoStorageConfig(t *testing.T) {
 	template := GetDatacenterConfig()
diff --git a/pkg/test/testenv.go b/pkg/test/testenv.go
index 30f9846f..32c5343c 100644
--- a/pkg/test/testenv.go
+++ b/pkg/test/testenv.go
@@ -3,6 +3,8 @@ package test
 import (
 	"context"
 	"fmt"
+	"github.com/go-logr/zapr"
+	"go.uber.org/zap"
 	"os"
 	"path/filepath"
 	"testing"
@@ -10,11 +12,9 @@ import (
 	reaperapi "github.com/k8ssandra/k8ssandra-operator/apis/reaper/v1alpha1"
 	promapi "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1"
 
-	"github.com/bombsimon/logrusr"
 	"github.com/k8ssandra/k8ssandra-operator/pkg/clientcache"
 	"github.com/k8ssandra/k8ssandra-operator/test/framework"
 	"github.com/k8ssandra/k8ssandra-operator/test/kustomize"
-	"github.com/sirupsen/logrus"
 	"k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/client-go/kubernetes/scheme"
 	"k8s.io/client-go/rest"
@@ -240,7 +240,11 @@ func (e *MultiClusterTestEnv) ControllerTest(ctx context.Context, test Controlle
 func TestSetup(t *testing.T) context.Context {
 	ctx := ctrl.SetupSignalHandler()
 
-	log := logrusr.NewLogger(logrus.New())
+	logger, err := zap.NewDevelopment()
+	if err != nil {
+		t.Fatalf("failed to created logger: %v", err)
+	}
+	log := zapr.NewLogger(logger)
 	logf.SetLogger(log)
 
 	if err := prepareCRDs(); err != nil {
diff --git a/test/kuttl/test-user-defined-ns/01-assert.yaml b/test/kuttl/test-user-defined-ns/01-assert.yaml
index 2b7469c8..795c8102 100644
--- a/test/kuttl/test-user-defined-ns/01-assert.yaml
+++ b/test/kuttl/test-user-defined-ns/01-assert.yaml
@@ -14,4 +14,20 @@ status:
   - reason: NewReplicaSetAvailable
     status: "True"
     type: Progressing
-  readyReplicas: 1
\ No newline at end of file
+  readyReplicas: 1
+---
+apiVersion: admissionregistration.k8s.io/v1
+kind: ValidatingWebhookConfiguration
+metadata:
+  annotations:
+    cert-manager.io/inject-ca-from: k8ssandra-operator/cass-operator-serving-cert
+  name: cass-operator-validating-webhook-configuration
+webhooks:
+  - admissionReviewVersions:
+      - v1
+      - v1beta1
+    clientConfig:
+      service:
+        name: cass-operator-webhook-service
+        namespace: k8ssandra-operator
+        path: /validate-cassandra-datastax-com-v1beta1-cassandradatacenter
diff --git a/test/kuttl/test-user-defined-ns/01-k8ssandra-operator.yaml b/test/kuttl/test-user-defined-ns/01-k8ssandra-operator.yaml
index 3cd240d9..860a77a6 100644
--- a/test/kuttl/test-user-defined-ns/01-k8ssandra-operator.yaml
+++ b/test/kuttl/test-user-defined-ns/01-k8ssandra-operator.yaml
@@ -1,6 +1,8 @@
+# We are deploying a cluster scoped version of cass-operator and k8ssandra-operator into the namespace k8ssandra-operator.
+# This sets us up to ensure the cluster-scope component is successful, and immediately demonstrates that the single-namespace component is sucessful.
 apiVersion: kuttl.dev/v1beta1
 kind: TestStep
 commands: 
 - script: |
     ../../../bin/kustomize build ../../../config/deployments/control-plane/cluster-scope | kubectl apply --server-side --force-conflicts -f -
-  ignoreFailure: false 
\ No newline at end of file
+  ignoreFailure: false 
diff --git a/test/kuttl/test-user-defined-ns/02-k8ssandra-cluster.yaml b/test/kuttl/test-user-defined-ns/02-k8ssandra-cluster.yaml
index f93c4a30..cfd41e8f 100644
--- a/test/kuttl/test-user-defined-ns/02-k8ssandra-cluster.yaml
+++ b/test/kuttl/test-user-defined-ns/02-k8ssandra-cluster.yaml
@@ -1,3 +1,4 @@
+# While all operators are in k8ssandra-operator ns, the K8ssandraCluster is in test-ns. This demonstrates that cluster scoping is successful.
 apiVersion: kuttl.dev/v1beta1
 kind: TestStep
 commands: