Adding a chRoot option, and chRoot validation/creation, for all ZK references. (#62)

- Fixed & Expanded docs
- Made resolving of labels & annotation changes better.
diff --git a/README.md b/README.md
index 0afff21..a3831a8 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,23 @@
 # Solr Operator
-[![Build Status](https://travis-ci.com/bloomberg/solr-operator.svg?branch=master)](https://travis-ci.com/bloomberg/solr-operator) [![Go Report Card](https://goreportcard.com/badge/github.com/bloomberg/solr-operator)](https://goreportcard.com/report/github.com/bloomberg/solr-operator) ![Latest Version](https://img.shields.io/github/tag/bloomberg/solr-operator) [![Docker Pulls](https://img.shields.io/docker/pulls/bloomberg/solr-operator)](https://hub.docker.com/r/bloomberg/solr-operator/)
+[![Latest Version](https://img.shields.io/github/tag/bloomberg/solr-operator)](https://github.com/bloomberg/solr-operator/releases)
+[![Build Status](https://travis-ci.com/bloomberg/solr-operator.svg?branch=master)](https://travis-ci.com/bloomberg/solr-operator)
+[![License](https://img.shields.io/badge/LICENSE-Apache2.0-ff69b4.svg)](http://www.apache.org/licenses/LICENSE-2.0.html)
+[![Go Report Card](https://goreportcard.com/badge/github.com/bloomberg/solr-operator)](https://goreportcard.com/report/github.com/bloomberg/solr-operator)
+[![Commit since last release](https://img.shields.io/github/commits-since/bloomberg/solr-operator/latest.svg)](https://github.com/bloomberg/solr-operator/commits/master)
+[![Docker Pulls](https://img.shields.io/docker/pulls/bloomberg/solr-operator)](https://hub.docker.com/r/bloomberg/solr-operator/)
+[![Slack](https://img.shields.io/badge/slack-join_chat-white.svg?logo=slack&style=social)](https://kubernetes.slack.com/messages/solr-operator)
 
 The __Solr Operator__ manages Apache Solr Clouds within Kubernetes. It is built on top of the [Kube Builder](https://github.com/kubernetes-sigs/kubebuilder) framework.
 
 The project is currently in beta (`v1beta1`), and while we do not anticipate changing the API in backwards-incompatible ways there is no such guarantee yet.
 
+If you run into issues using the Solr Operator, please:
+- Reference the [version compatibility and upgrade notes](#version-compatability--upgrade-notes) provided below
+- Create a Github Issue in this repo, describing your problem with as much detail as possible
+- Reach out on our Slack channel!
+
+Join us on the [#solr-operator](https://kubernetes.slack.com/messages/solr-operator) channel in the official Kubernetes slack workspace.
+
 ## Menu
 
 - [Getting Started](#getting-started)
@@ -13,6 +26,7 @@
     - [Solr Backups](#solr-backups)
     - [Solr Metrics](#solr-prometheus-exporter)
 - [Contributions](#contributions)
+- [Version Compatibility and Upgrade Notes](#version-compatability--upgrade-notes)
 - [License](#license)
 - [Code of Conduct](#code-of-conduct)
 - [Security Vulnerability Reporting](#security-vulnerability-reporting)
@@ -133,6 +147,38 @@
 solrcloud.solr.bloomberg.com/example       8.1.1     4              4       4            47h
 ```
 
+## Zookeeper Reference
+
+Solr Clouds require an Apache Zookeeper to connect to.
+
+The Solr operator gives a few options.
+
+**Note** - Both options below come with options to specify a `chroot`, or a ZNode path for solr to use as it's base "directory" in Zookeeper. Before the operator creates or updates a StatefulSet with a given `chroot`, it will first ensure that the given ZNode path exists and if it doesn't the operator will create all necessary ZNodes in the path. If no chroot is given, a default of `/` will be used, which doesn't require the existence check previously mentioned. If a chroot is provided without a prefix of `/`, the operator will add the prefix, as it is required by Zookeeper.
+
+### ZK Connection Info
+
+This is an external/internal connection string as well as an optional chRoot to an already running Zookeeeper ensemble.
+If you provide an external connection string, you do not _have_ to provide an internal one as well.
+
+### Provided Instance
+
+If you do not require the Solr cloud to run cross-kube cluster, and do not want to manage your own Zookeeper ensemble,
+the solr-operator can manage Zookeeper ensemble(s) for you.
+
+#### Zookeeper
+
+Using the [zookeeper-operator](https://github.com/pravega/zookeeper-operator), a new Zookeeper ensemble can be spun up for 
+each solrCloud that has this option specified.
+
+The startup parameter `zookeeper-operator` must be provided on startup of the solr-operator for this parameter to be available.
+
+#### Zetcd
+
+Using [etcd-operator](https://github.com/coreos/etcd-operator), a new Etcd ensemble can be spun up for each solrCloud that has this option specified.
+A [Zetcd](https://github.com/etcd-io/zetcd) deployment is also created so that Solr can interact with Etcd as if it were a Zookeeper ensemble.
+
+The startup parameter `etcd-operator` must be provided on startup of the solr-operator for this parameter to be available.
+
 ### Solr Collections
 
 Solr-operator can manage the creation, deletion and modification of Solr collections. 
@@ -214,38 +260,6 @@
 Routed aliases are presently not supported
 
   
-## Zookeeper
-=======
-### Zookeeper Reference
-
-Solr Clouds require an Apache Zookeeper to connect to.
-
-The Solr operator gives a few options.
-
-#### ZK Connection Info
-
-This is an external/internal connection string as well as an optional chRoot to an already running Zookeeeper ensemble.
-If you provide an external connection string, you do not _have_ to provide an internal one as well.
-
-#### Provided Instance
-
-If you do not require the Solr cloud to run cross-kube cluster, and do not want to manage your own Zookeeper ensemble,
-the solr-operator can manage Zookeeper ensemble(s) for you.
-
-##### Zookeeper
-
-Using the [zookeeper-operator](https://github.com/pravega/zookeeper-operator), a new Zookeeper ensemble can be spun up for 
-each solrCloud that has this option specified.
-
-The startup parameter `zookeeper-operator` must be provided on startup of the solr-operator for this parameter to be available.
-
-##### Zetcd
-
-Using [etcd-operator](https://github.com/coreos/etcd-operator), a new Etcd ensemble can be spun up for each solrCloud that has this option specified.
-A [Zetcd](https://github.com/etcd-io/zetcd) deployment is also created so that Solr can interact with Etcd as if it were a Zookeeper ensemble.
-
-The startup parameter `etcd-operator` must be provided on startup of the solr-operator for this parameter to be available.
-
 ## Solr Backups
 
 Solr backups require 3 things:
@@ -388,12 +402,16 @@
 5. Navigate to your browser: http://default-example-solrcloud.ing.local.domain/solr/#/ to validate everything is working
 
 
-## Version Compatability & Changes
+## Version Compatibility & Upgrade Notes
+
+#### v0.2.1
+- The zkConnectionString used for provided zookeepers changed from using the string provided in the `ZkCluster.Status`, which used an IP, to using the service name. This will cause a rolling restart of your solrs using the provided zookeeper option, but there will be no data loss.
 
 #### v0.2.0
 - Uses `gomod` instead of `dep`
 - `SolrCloud.zookeeperRef.provided.zookeeper.persistentVolumeClaimSpec` has been deprecated in favor of the `SolrCloud.zookeeperRef.provided.zookeeper.persistence` option.  
 This option is backwards compatible, but will be removed in a future version.
+- An upgrade to the ZKOperator version `0.2.4` is required.
 
 #### v0.1.1
 - `SolrCloud.Spec.persistentVolumeClaim` was renamed to `SolrCloud.Spec.dataPvcSpec`
diff --git a/api/v1beta1/solrcloud_types.go b/api/v1beta1/solrcloud_types.go
index d95c9cc..28c947b 100644
--- a/api/v1beta1/solrcloud_types.go
+++ b/api/v1beta1/solrcloud_types.go
@@ -264,10 +264,8 @@
 
 func (ci *ZookeeperConnectionInfo) withDefaults() (changed bool) {
 	if ci.InternalConnectionString == "" {
-		changed = true
-		if ci.ExternalConnectionString == nil {
-			ci.InternalConnectionString = "N/A"
-		} else {
+		if ci.ExternalConnectionString != nil {
+			changed = true
 			ci.InternalConnectionString = *ci.ExternalConnectionString
 		}
 	}
@@ -296,6 +294,10 @@
 	//   - An etcd operator to be running
 	// +optional
 	Zetcd *FullZetcdSpec `json:"zetcd,inline"`
+
+	// The ChRoot to connect solr at
+	// +optional
+	ChRoot string `json:"chroot,omitempty"`
 }
 
 func (z *ProvidedZookeeper) withDefaults() (changed bool) {
@@ -309,6 +311,14 @@
 	if z.Zetcd != nil {
 		changed = z.Zetcd.withDefaults() || changed
 	}
+
+	if z.ChRoot == "" {
+		changed = true
+		z.ChRoot = "/"
+	} else if !strings.HasPrefix(z.ChRoot, "/") {
+		changed = true
+		z.ChRoot = "/" + z.ChRoot
+	}
 	return changed
 }
 
diff --git a/config/crd/bases/solr.bloomberg.com_solrclouds.yaml b/config/crd/bases/solr.bloomberg.com_solrclouds.yaml
index 1292068..4ad4170 100644
--- a/config/crd/bases/solr.bloomberg.com_solrclouds.yaml
+++ b/config/crd/bases/solr.bloomberg.com_solrclouds.yaml
@@ -2040,6 +2040,9 @@
                   description: 'A zookeeper that is created by the solr operator Note:
                     This option will not allow the SolrCloud to run across kube-clusters.'
                   properties:
+                    chroot:
+                      description: The ChRoot to connect solr at
+                      type: string
                     etcdSpec:
                       description: EtcdSpec defines the internal etcd ensemble to
                         run for solr (spoofing zookeeper)
diff --git a/controllers/controller_utils_test.go b/controllers/controller_utils_test.go
index 6464114..8183a95 100644
--- a/controllers/controller_utils_test.go
+++ b/controllers/controller_utils_test.go
@@ -17,13 +17,17 @@
 package controllers
 
 import (
+	solr "github.com/bloomberg/solr-operator/api/v1beta1"
 	"github.com/onsi/gomega"
 	"github.com/stretchr/testify/assert"
 	"golang.org/x/net/context"
 	appsv1 "k8s.io/api/apps/v1"
+	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	extv1 "k8s.io/api/extensions/v1beta1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/types"
+	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	"testing"
 )
@@ -49,6 +53,12 @@
 	return stateful
 }
 
+func expectNoStatefulSet(g *gomega.GomegaWithT, statefulSetKey types.NamespacedName) {
+	stateful := &appsv1.StatefulSet{}
+	g.Eventually(func() error { return testClient.Get(context.TODO(), statefulSetKey, stateful) }, timeout).
+		Should(gomega.MatchError("StatefulSet.apps \"" + statefulSetKey.Name + "\" not found"))
+}
+
 func expectService(t *testing.T, g *gomega.GomegaWithT, requests chan reconcile.Request, expectedRequest reconcile.Request, serviceKey types.NamespacedName, selectorLables map[string]string) *corev1.Service {
 	service := &corev1.Service{}
 	g.Eventually(func() error { return testClient.Get(context.TODO(), serviceKey, service) }, timeout).
@@ -150,3 +160,49 @@
 
 	return deploy
 }
+
+func testPodEnvVariables(t *testing.T, expectedEnvVars map[string]string, foundEnvVars []corev1.EnvVar) {
+	matchCount := 0
+	for _, envVar := range foundEnvVars {
+		if expectedVal, match := expectedEnvVars[envVar.Name]; match {
+			matchCount += 1
+			assert.Equal(t, expectedVal, envVar.Value, "Wrong value for env variable '%s' in podSpec", envVar.Name)
+		}
+	}
+	assert.Equal(t, len(expectedEnvVars), matchCount, "Not all expected env variables found in podSpec")
+}
+
+func cleanupTest(g *gomega.GomegaWithT, namespace string) {
+	deleteOpts := []client.DeleteAllOfOption{
+		client.InNamespace(namespace),
+	}
+
+	cleanupObjects := []runtime.Object{
+		// Solr Operator CRDs, modify this list whenever CRDs are added/deleted
+		&solr.SolrCloud{}, &solr.SolrBackup{}, &solr.SolrCollection{}, &solr.SolrCollectionAlias{}, &solr.SolrPrometheusExporter{},
+
+		// All dependent Kubernetes types, in order of dependence (deployment then replicaSet then pod)
+		&corev1.ConfigMap{}, &batchv1.Job{}, &extv1.Ingress{},
+		&corev1.PersistentVolumeClaim{}, &corev1.PersistentVolume{},
+		&appsv1.StatefulSet{}, &appsv1.Deployment{}, &appsv1.ReplicaSet{}, &corev1.Pod{},
+	}
+	cleanupTestObjects(g, namespace, deleteOpts, cleanupObjects)
+
+	// Delete all Services separately (https://github.com/kubernetes/kubernetes/pull/85802#issuecomment-561239845)
+	opts := []client.ListOption{
+		client.InNamespace(namespace),
+	}
+	services := &corev1.ServiceList{}
+	g.Eventually(func() error { return testClient.List(context.TODO(), services, opts...) }, timeout).Should(gomega.Succeed())
+
+	for _, item := range services.Items {
+		g.Eventually(func() error { return testClient.Delete(context.TODO(), &item) }, timeout).Should(gomega.Succeed())
+	}
+}
+
+func cleanupTestObjects(g *gomega.GomegaWithT, namespace string, deleteOpts []client.DeleteAllOfOption, objects []runtime.Object) {
+	// Delete all SolrClouds
+	for _, obj := range objects {
+		g.Eventually(func() error { return testClient.DeleteAllOf(context.TODO(), obj, deleteOpts...) }, timeout).Should(gomega.Succeed())
+	}
+}
diff --git a/controllers/solrcloud_controller.go b/controllers/solrcloud_controller.go
index 6780ea8..fb44199 100644
--- a/controllers/solrcloud_controller.go
+++ b/controllers/solrcloud_controller.go
@@ -38,6 +38,7 @@
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 	"sort"
 	"strings"
+	"time"
 )
 
 // SolrCloudReconciler reconciles a SolrCloud object
@@ -108,6 +109,9 @@
 		return reconcile.Result{Requeue: true}, nil
 	}
 
+	// When working with the clouds, some actions outside of kube may need to be retried after a few seconds
+	requeueOrNot := reconcile.Result{}
+
 	newStatus := solr.SolrCloudStatus{}
 
 	busyBoxImage := *instance.Spec.BusyBoxImage
@@ -115,13 +119,13 @@
 	blockReconciliationOfStatefulSet := false
 
 	if err := reconcileZk(r, req, instance, busyBoxImage, &newStatus); err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	// Generate Service
 	service := util.GenerateService(instance)
 	if err := controllerutil.SetControllerReference(instance, service, r.scheme); err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	// Check if the Service already exists
@@ -138,7 +142,7 @@
 		}
 		newStatus.InternalCommonAddress = "http://" + foundService.Name + "." + foundService.Namespace
 	} else {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	solrNodeNames := instance.GetAllSolrNodeNames()
@@ -148,7 +152,7 @@
 	for _, nodeName := range solrNodeNames {
 		err, ip := reconcileNodeService(r, instance, nodeName)
 		if err != nil {
-			return reconcile.Result{}, err
+			return requeueOrNot, err
 		}
 		if IngressBaseUrl != "" {
 			if ip == "" {
@@ -163,7 +167,7 @@
 	// Generate HeadlessService
 	headless := util.GenerateHeadlessService(instance)
 	if err := controllerutil.SetControllerReference(instance, headless, r.scheme); err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	// Check if the HeadlessService already exists
@@ -178,13 +182,13 @@
 		err = r.Update(context.TODO(), foundHeadless)
 	}
 	if err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	// Generate ConfigMap
 	configMap := util.GenerateConfigMap(instance)
 	if err := controllerutil.SetControllerReference(instance, configMap, r.scheme); err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	// Check if the ConfigMap already exists
@@ -199,7 +203,7 @@
 		err = r.Update(context.TODO(), foundConfigMap)
 	}
 	if err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	// Only create stateful set if zkConnectionString can be found (must contain host and port)
@@ -211,17 +215,34 @@
 		// Generate StatefulSet
 		statefulSet := util.GenerateStatefulSet(instance, &newStatus, IngressBaseUrl, hostNameIpMap)
 		if err := controllerutil.SetControllerReference(instance, statefulSet, r.scheme); err != nil {
-			return reconcile.Result{}, err
+			return requeueOrNot, err
 		}
 
 		// Check if the StatefulSet already exists
 		foundStatefulSet := &appsv1.StatefulSet{}
 		err = r.Get(context.TODO(), types.NamespacedName{Name: statefulSet.Name, Namespace: statefulSet.Namespace}, foundStatefulSet)
 		if err != nil && errors.IsNotFound(err) {
-			r.Log.Info("Creating StatefulSet", "namespace", statefulSet.Namespace, "name", statefulSet.Name)
-			err = r.Create(context.TODO(), statefulSet)
+			// Before creating the statefulSet, we must first check that the  ZkConnection String is usable
+			if zkErr := ensureZkChrootExists(r, instance, newStatus.ZookeeperConnectionInfo, &requeueOrNot); zkErr == nil {
+				r.Log.Info("Creating StatefulSet", "namespace", statefulSet.Namespace, "name", statefulSet.Name)
+				err = r.Create(context.TODO(), statefulSet)
+			} else {
+				// Erase the "StatefulSet not found" error, so that we can continue reconciling
+				err = nil
+				r.Log.Info("Cannot create StatefulSet until zkConnectionString & chroot have been ensured to exist.", "namespace", statefulSet.Namespace, "name", statefulSet.Name)
+			}
 		} else if err == nil {
-			if util.CopyStatefulSetFields(statefulSet, foundStatefulSet) {
+			updateSS := true
+			// If the statefulSet is using the wrong ZkConnectionString, we must first check that the new ZkConnection String is usable
+			if foundStatefulSet.Annotations[util.SolrZKConnectionStringAnnotation] != statefulSet.Annotations[util.SolrZKConnectionStringAnnotation] {
+				if zkErr := ensureZkChrootExists(r, instance, newStatus.ZookeeperConnectionInfo, &requeueOrNot); zkErr == nil {
+					updateSS = true
+				} else {
+					updateSS = false
+					r.Log.Info("Solr has a new ZkConnectionString, cannot update StatefulSet until new zkConnectionString & chroot have been ensured to exist.", "namespace", statefulSet.Namespace, "name", statefulSet.Name)
+				}
+			}
+			if util.CopyStatefulSetFields(statefulSet, foundStatefulSet) && updateSS {
 				// Update the found StatefulSet and write the result back if there are any changes
 				r.Log.Info("Updating StatefulSet", "namespace", statefulSet.Namespace, "name", statefulSet.Name)
 				err = r.Update(context.TODO(), foundStatefulSet)
@@ -230,20 +251,20 @@
 			newStatus.ReadyReplicas = foundStatefulSet.Status.ReadyReplicas
 		}
 		if err != nil {
-			return reconcile.Result{}, err
+			return requeueOrNot, err
 		}
 	}
 
 	err = reconcileCloudStatus(r, instance, &newStatus)
 	if err != nil {
-		return reconcile.Result{}, err
+		return requeueOrNot, err
 	}
 
 	if IngressBaseUrl != "" {
 		// Generate Ingress
 		ingress := util.GenerateCommonIngress(instance, solrNodeNames, IngressBaseUrl)
 		if err := controllerutil.SetControllerReference(instance, ingress, r.scheme); err != nil {
-			return reconcile.Result{}, err
+			return requeueOrNot, err
 		}
 
 		// Check if the Ingress already exists
@@ -258,7 +279,7 @@
 			err = r.Update(context.TODO(), foundIngress)
 		}
 		if err != nil {
-			return reconcile.Result{}, err
+			return requeueOrNot, err
 		} else {
 			address := "http://" + instance.CommonIngressUrl(IngressBaseUrl)
 			newStatus.ExternalCommonAddress = &address
@@ -268,13 +289,13 @@
 	if !reflect.DeepEqual(instance.Status, newStatus) {
 		instance.Status = newStatus
 		r.Log.Info("Updating SolrCloud Status: ", "namespace", instance.Namespace, "name", instance.Name)
-		err = r.Status().Update(context.Background(), instance)
+		err = r.Status().Update(context.TODO(), instance)
 		if err != nil {
-			return reconcile.Result{}, err
+			return requeueOrNot, err
 		}
 	}
 
-	return reconcile.Result{}, nil
+	return requeueOrNot, nil
 }
 
 func reconcileCloudStatus(r *SolrCloudReconciler, solrCloud *solr.SolrCloud, newStatus *solr.SolrCloudStatus) (err error) {
@@ -450,10 +471,6 @@
 			if err != nil && errors.IsNotFound(err) {
 				r.Log.Info("Creating Zetcd Service", "namespace", service.Namespace, "name", service.Name)
 				err = r.Create(context.TODO(), service)
-				newStatus.ZookeeperConnectionInfo = solr.ZookeeperConnectionInfo{
-					InternalConnectionString: service.Name + "." + service.Namespace + ":2181",
-					ChRoot:                   "/",
-				}
 			} else if err == nil {
 				if util.CopyServiceFields(service, foundService) {
 					// Update the found Zetcd Service and write the result back if there are any changes
@@ -462,12 +479,10 @@
 				}
 				newStatus.ZookeeperConnectionInfo = solr.ZookeeperConnectionInfo{
 					InternalConnectionString: service.Name + "." + service.Namespace + ":2181",
-					ChRoot:                   "/",
+					ChRoot:                   pzk.ChRoot,
 				}
-			} else {
-				return err
 			}
-
+			return err
 		} else if pzk.Zookeeper != nil {
 			// Generate ZookeeperCluster
 			if !useZkCRD {
@@ -495,18 +510,28 @@
 					external = nil
 				}
 				newStatus.ZookeeperConnectionInfo = solr.ZookeeperConnectionInfo{
-					InternalConnectionString: foundZkCluster.Status.InternalClientEndpoint,
+					InternalConnectionString: fmt.Sprintf("%s:%d", foundZkCluster.GetClientServiceName(), foundZkCluster.ZookeeperPorts().Client),
 					ExternalConnectionString: external,
-					ChRoot:                   "/",
+					ChRoot:                   pzk.ChRoot,
 				}
-			} else {
-				return err
 			}
+			return err
 		}
 	}
 	return nil
 }
 
+func ensureZkChrootExists(r *SolrCloudReconciler, solr *solr.SolrCloud, info solr.ZookeeperConnectionInfo, requeueOrNot *reconcile.Result) error {
+	err := util.CreateChRootIfNecessary(info)
+	if err != nil {
+		r.Log.Error(err, "Zk or Chroot has changed, cannot create new ZK Chroot for Solr Cloud", "namespace", solr.Namespace, "name", solr.Name)
+		requeueOrNot.RequeueAfter = time.Second * 5
+
+		// TODO: Create an event for the SolrCloud so that users understand why the StatefulSet hasn't been updated/created.
+	}
+	return err
+}
+
 func (r *SolrCloudReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	return r.SetupWithManagerAndReconciler(mgr, r)
 }
diff --git a/controllers/solrcloud_controller_test.go b/controllers/solrcloud_controller_test.go
index 88c0e2a..2e0330a 100644
--- a/controllers/solrcloud_controller_test.go
+++ b/controllers/solrcloud_controller_test.go
@@ -55,6 +55,9 @@
 					InternalConnectionString: "host:7271",
 				},
 			},
+			SolrJavaMem:  "-Xmx4G",
+			SolrOpts:     "extra-opts",
+			SolrLogLevel: "DEBUG",
 		},
 	}
 
@@ -78,6 +81,8 @@
 		mgrStopped.Wait()
 	}()
 
+	cleanupTest(g, instance.Namespace)
+
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
 	// The instance object may not be a valid object because it might be missing some required fields.
@@ -93,6 +98,17 @@
 	// Check the statefulSet
 	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
 
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet requires a container.")
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":        "host:7271/",
+		"SOLR_HOST":      "$(POD_HOSTNAME)." + instance.HeadlessServiceName(),
+		"SOLR_JAVA_MEM":  "-Xmx4G",
+		"SOLR_PORT":      "8983",
+		"SOLR_LOG_LEVEL": "DEBUG",
+		"SOLR_OPTS":      "extra-opts",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+
 	// Check the client Service
 	expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
 
@@ -104,7 +120,8 @@
 }
 
 func TestCloudReconcileWithIngress(t *testing.T) {
-	SetIngressBaseUrl("ing.base.domain")
+	ingressBaseDomain := "ing.base.domain"
+	SetIngressBaseUrl(ingressBaseDomain)
 	UseEtcdCRD(false)
 	UseZkCRD(true)
 	g := gomega.NewGomegaWithT(t)
@@ -116,6 +133,152 @@
 					InternalConnectionString: "host:7271",
 				},
 			},
+			SolrGCTune: "gc Options",
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	// The instance object may not be a valid object because it might be missing some required fields.
+	// Please modify the instance object by adding required fields and then remove the following if statement.
+	if apierrors.IsInvalid(err) {
+		t.Logf("failed to create object, got an invalid object error: %v", err)
+		return
+	}
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the services will have IP addresses for the hostAliases to use
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	// Check the statefulSet
+	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+
+	assert.Equal(t, 1, len(statefulSet.Spec.Template.Spec.Containers), "Solr StatefulSet require a container.")
+	expectedEnvVars := map[string]string{
+		"ZK_HOST":   "host:7271/",
+		"SOLR_HOST": instance.NodeIngressUrl("$(POD_HOSTNAME)", ingressBaseDomain),
+		"SOLR_PORT": "8983",
+		"GC_TUNE":   "gc Options",
+	}
+	testPodEnvVariables(t, expectedEnvVars, statefulSet.Spec.Template.Spec.Containers[0].Env)
+
+	// Check the client Service
+	expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+
+	// Check the headless Service
+	expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
+
+	// Check the ingress
+	expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+}
+
+func TestCloudWithProvidedZookeeperReconcile(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(true)
+	g := gomega.NewGomegaWithT(t)
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ProvidedZookeeper: &solr.ProvidedZookeeper{
+					ChRoot:    "a-ch/root",
+					Zookeeper: &solr.ZookeeperSpec{},
+				},
+			},
+		},
+	}
+
+	// Setup the Manager and Controller.  Wrap the Controller Reconcile function so it writes each request to a
+	// channel when it is finished.
+	mgr, err := manager.New(testCfg, manager.Options{})
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	testClient = mgr.GetClient()
+
+	// Blocked until https://github.com/pravega/zookeeper-operator/pull/99 is merged
+	//g.Expect(zookeepercluster.AddZookeeperReconciler(mgr)).NotTo(gomega.HaveOccurred())
+
+	solrCloudReconciler := &SolrCloudReconciler{
+		Client: testClient,
+		Log:    ctrl.Log.WithName("controllers").WithName("SolrCloud"),
+	}
+	newRec, requests := SetupTestReconcile(solrCloudReconciler)
+	g.Expect(solrCloudReconciler.SetupWithManagerAndReconciler(mgr, newRec)).NotTo(gomega.HaveOccurred())
+
+	stopMgr, mgrStopped := StartTestManager(mgr, g)
+
+	defer func() {
+		close(stopMgr)
+		mgrStopped.Wait()
+	}()
+
+	cleanupTest(g, instance.Namespace)
+
+	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
+	err = testClient.Create(context.TODO(), instance)
+	// The instance object may not be a valid object because it might be missing some required fields.
+	// Please modify the instance object by adding required fields and then remove the following if statement.
+	if apierrors.IsInvalid(err) {
+		t.Logf("failed to create object, got an invalid object error: %v", err)
+		return
+	}
+	g.Expect(err).NotTo(gomega.HaveOccurred())
+	defer testClient.Delete(context.TODO(), instance)
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the zkCluster will have been created
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+
+	g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
+
+	// Check that the ZkConnectionInformation is correct
+	assert.Equal(t, instance.ProvidedZookeeperName()+"-client:2181", instance.Status.ZookeeperConnectionInfo.InternalConnectionString, "Wrong zkConnectionString in status")
+	assert.Equal(t, "/a-ch/root", instance.Status.ZookeeperConnectionInfo.ChRoot, "Wrong zk chRoot in status")
+	assert.Nil(t, instance.Status.ZookeeperConnectionInfo.ExternalConnectionString, "Since a provided zk is used, the externalConnectionString in the status should be Nil")
+
+	// Check that the statefulSet has not been created, because the ZkChRoot is not able to be created or verified
+	expectNoStatefulSet(g, cloudSsKey)
+}
+
+func TestCloudWithExternalZookeeperChroot(t *testing.T) {
+	SetIngressBaseUrl("")
+	UseEtcdCRD(false)
+	UseZkCRD(true)
+	g := gomega.NewGomegaWithT(t)
+	instance := &solr.SolrCloud{
+		ObjectMeta: metav1.ObjectMeta{Name: expectedCloudRequest.Name, Namespace: expectedCloudRequest.Namespace},
+		Spec: solr.SolrCloudSpec{
+			ZookeeperRef: &solr.ZookeeperRef{
+				ConnectionInfo: &solr.ZookeeperConnectionInfo{
+					ChRoot:                   "a-ch/root",
+					InternalConnectionString: "host:7271,host2:7271",
+				},
+			},
 		},
 	}
 
@@ -139,6 +302,8 @@
 		mgrStopped.Wait()
 	}()
 
+	cleanupTest(g, instance.Namespace)
+
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
 	// The instance object may not be a valid object because it might be missing some required fields.
@@ -150,18 +315,21 @@
 	g.Expect(err).NotTo(gomega.HaveOccurred())
 	defer testClient.Delete(context.TODO(), instance)
 	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	// Add an additional check for reconcile, so that the zkCluster will have been created
+	// Otherwise the reconciler will have 'blockReconciliationOfStatefulSet' set to true, and the stateful set will not be created
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
+	g.Eventually(requests, timeout).Should(gomega.Receive(gomega.Equal(expectedCloudRequest)))
 
-	// Check the statefulSet
-	statefulSet := expectStatefulSet(t, g, requests, expectedCloudRequest, cloudSsKey)
+	g.Eventually(func() error { return testClient.Get(context.TODO(), expectedCloudRequest.NamespacedName, instance) }, timeout).Should(gomega.Succeed())
 
-	// Check the client Service
-	expectService(t, g, requests, expectedCloudRequest, cloudCsKey, statefulSet.Spec.Template.Labels)
+	// Check that the ZkConnectionInformation is correct
+	assert.Equal(t, "host:7271,host2:7271", instance.Status.ZookeeperConnectionInfo.InternalConnectionString, "Wrong internal zkConnectionString in status")
+	assert.Equal(t, "host:7271,host2:7271", instance.Status.ZookeeperConnectionInfo.InternalConnectionString, "Wrong external zkConnectionString in status")
+	assert.Equal(t, "/a-ch/root", instance.Status.ZookeeperConnectionInfo.ChRoot, "Wrong zk chRoot in status")
+	assert.Equal(t, "host:7271,host2:7271/a-ch/root", instance.Status.ZookeeperConnectionInfo.ZkConnectionString(), "Wrong zkConnectionString())")
 
-	// Check the headless Service
-	expectService(t, g, requests, expectedCloudRequest, cloudHsKey, statefulSet.Spec.Template.Labels)
-
-	// Check the ingress
-	expectIngress(g, requests, expectedCloudRequest, cloudIKey)
+	// Check that the statefulSet has not been created, because the ZkChRoot is not able to be created or verified
+	expectNoStatefulSet(g, cloudSsKey)
 }
 
 func TestDefaults(t *testing.T) {
@@ -205,6 +373,8 @@
 		mgrStopped.Wait()
 	}()
 
+	cleanupTest(g, instance.Namespace)
+
 	// Create the SolrCloud object and expect the Reconcile and StatefulSet to be created
 	err = testClient.Create(context.TODO(), instance)
 	// The instance object may not be a valid object because it might be missing some required fields.
diff --git a/controllers/util/common.go b/controllers/util/common.go
new file mode 100644
index 0000000..2ec1444
--- /dev/null
+++ b/controllers/util/common.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2019 Bloomberg Finance LP.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package util
+
+import (
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+// CopyLabelsAndAnnotations copies the labels and annotations from one object to another.
+// Additional Labels and Annotations in the 'to' object will not be removed.
+// Returns true if there are updates required to the object.
+func CopyLabelsAndAnnotations(from, to *metav1.ObjectMeta) (requireUpdate bool) {
+	if len(to.Labels) == 0 && len(from.Labels) > 0 {
+		to.Labels = map[string]string{}
+	}
+	for k, v := range from.Labels {
+		if to.Labels[k] != v {
+			requireUpdate = true
+			log.Info("Update Label", "label", k, "newValue", v, "oldValue", to.Labels[k])
+			to.Labels[k] = v
+		}
+	}
+
+	if len(to.Annotations) == 0 && len(from.Annotations) > 0 {
+		to.Annotations = map[string]string{}
+	}
+	for k, v := range from.Annotations {
+		if to.Annotations[k] != v {
+			requireUpdate = true
+			log.Info("Update Annotation", "annotation", k, "newValue", v, "oldValue", to.Annotations[k])
+			to.Annotations[k] = v
+		}
+	}
+
+	return requireUpdate
+}
diff --git a/controllers/util/prometheus_exporter_util.go b/controllers/util/prometheus_exporter_util.go
index 7e4f057..1c45b43 100644
--- a/controllers/util/prometheus_exporter_util.go
+++ b/controllers/util/prometheus_exporter_util.go
@@ -186,20 +186,7 @@
 
 // CopyConfigMapFields copies the owned fields from one ConfigMap to another
 func CopyMetricsConfigMapFields(from, to *corev1.ConfigMap) bool {
-	requireUpdate := false
-	for k, v := range from.Labels {
-		if to.Labels[k] != v {
-			requireUpdate = true
-		}
-		to.Labels[k] = v
-	}
-
-	for k, v := range from.Annotations {
-		if to.Annotations[k] != v {
-			requireUpdate = true
-		}
-		to.Annotations[k] = v
-	}
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	// Don't copy the entire Spec, because we can't overwrite the clusterIp field
 
diff --git a/controllers/util/solr_util.go b/controllers/util/solr_util.go
index 5b80820..df30268 100644
--- a/controllers/util/solr_util.go
+++ b/controllers/util/solr_util.go
@@ -41,6 +41,8 @@
 	ExtSolrClientPort     = 80
 	ExtSolrClientPortName = "ext-solr-client"
 	BackupRestoreVolume   = "backup-restore"
+
+	SolrZKConnectionStringAnnotation = "solr.apache.org/zkConnectionString"
 )
 
 // GenerateStatefulSet returns a new appsv1.StatefulSet pointer generated for the SolrCloud instance
@@ -59,6 +61,10 @@
 	labels["technology"] = solr.SolrTechnologyLabel
 	selectorLabels["technology"] = solr.SolrTechnologyLabel
 
+	annotations := map[string]string{
+		SolrZKConnectionStringAnnotation: solrCloudStatus.ZkConnectionString(),
+	}
+
 	solrVolumes := []corev1.Volume{
 		{
 			Name: "solr-xml",
@@ -136,9 +142,10 @@
 
 	stateful := &appsv1.StatefulSet{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      solrCloud.StatefulSetName(),
-			Namespace: solrCloud.GetNamespace(),
-			Labels:    labels,
+			Name:        solrCloud.StatefulSetName(),
+			Namespace:   solrCloud.GetNamespace(),
+			Labels:      labels,
+			Annotations: annotations,
 		},
 		Spec: appsv1.StatefulSetSpec{
 			Selector: &metav1.LabelSelector{
@@ -290,23 +297,7 @@
 // CopyStatefulSetFields copies the owned fields from one StatefulSet to another
 // Returns true if the fields copied from don't match to.
 func CopyStatefulSetFields(from, to *appsv1.StatefulSet) bool {
-
-	requireUpdate := false
-	for k, v := range from.Labels {
-		if to.Labels[k] != v {
-			requireUpdate = true
-			log.Info("Update SS", "diff", "labels", "label", k, v, to.Labels[k])
-		}
-		to.Labels[k] = v
-	}
-
-	for k, v := range from.Annotations {
-		if to.Annotations[k] != v {
-			requireUpdate = true
-			log.Info("Update SS", "diff", "annotations", "annotation", k, v, to.Annotations[k])
-		}
-		to.Annotations[k] = v
-	}
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	if !reflect.DeepEqual(to.Spec.Replicas, from.Spec.Replicas) {
 		requireUpdate = true
@@ -426,20 +417,7 @@
 
 // CopyConfigMapFields copies the owned fields from one ConfigMap to another
 func CopyConfigMapFields(from, to *corev1.ConfigMap) bool {
-	requireUpdate := false
-	for k, v := range from.Labels {
-		if to.Labels[k] != v {
-			requireUpdate = true
-		}
-		to.Labels[k] = v
-	}
-
-	for k, v := range from.Annotations {
-		if to.Annotations[k] != v {
-			requireUpdate = true
-		}
-		to.Annotations[k] = v
-	}
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	// Don't copy the entire Spec, because we can't overwrite the clusterIp field
 
@@ -542,20 +520,7 @@
 
 // CopyServiceFields copies the owned fields from one Service to another
 func CopyServiceFields(from, to *corev1.Service) bool {
-	requireUpdate := false
-	for k, v := range from.Labels {
-		if to.Labels[k] != v {
-			requireUpdate = true
-		}
-		to.Labels[k] = v
-	}
-
-	for k, v := range from.Annotations {
-		if to.Annotations[k] != v {
-			requireUpdate = true
-		}
-		to.Annotations[k] = v
-	}
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	// Don't copy the entire Spec, because we can't overwrite the clusterIp field
 
@@ -650,20 +615,7 @@
 
 // CopyIngressFields copies the owned fields from one Ingress to another
 func CopyIngressFields(from, to *extv1.Ingress) bool {
-	requireUpdate := false
-	for k, v := range from.Labels {
-		if to.Labels[k] != v {
-			requireUpdate = true
-		}
-		to.Labels[k] = v
-	}
-
-	for k, v := range from.Annotations {
-		if to.Annotations[k] != v {
-			requireUpdate = true
-		}
-		to.Annotations[k] = v
-	}
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	// Don't copy the entire Spec, because we can't overwrite the clusterIp field
 
diff --git a/controllers/util/zk_util.go b/controllers/util/zk_util.go
index 3d6addc..2db3391 100644
--- a/controllers/util/zk_util.go
+++ b/controllers/util/zk_util.go
@@ -18,10 +18,13 @@
 
 import (
 	"reflect"
+	"strings"
+	"time"
 
 	solr "github.com/bloomberg/solr-operator/api/v1beta1"
 	etcd "github.com/coreos/etcd-operator/pkg/apis/etcd/v1beta2"
 	zk "github.com/pravega/zookeeper-operator/pkg/apis/zookeeper/v1beta1"
+	zkCli "github.com/samuel/go-zookeeper/zk"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -54,6 +57,20 @@
 			Labels:      labels,
 			Replicas:    *zkSpec.Replicas,
 			Persistence: zkSpec.Persistence,
+			Ports: []corev1.ContainerPort{
+				{
+					Name:          "client",
+					ContainerPort: 2181,
+				},
+				{
+					Name:          "quorum",
+					ContainerPort: 2888,
+				},
+				{
+					Name:          "leader-election",
+					ContainerPort: 3888,
+				},
+			},
 		},
 	}
 
@@ -72,22 +89,7 @@
 // CopyZookeeperClusterFields copies the owned fields from one ZookeeperCluster to another
 // Returns true if the fields copied from don't match to.
 func CopyZookeeperClusterFields(from, to *zk.ZookeeperCluster) bool {
-	requireUpdate := false
-	for k, v := range to.Labels {
-		if from.Labels[k] != v {
-			log.Info("Updating Zookeeper label ", k, v)
-			requireUpdate = true
-		}
-	}
-	to.Labels = from.Labels
-
-	for k, v := range to.Annotations {
-		if from.Annotations[k] != v {
-			log.Info("Updating Zk annotation", k, v)
-			requireUpdate = true
-		}
-	}
-	to.Annotations = from.Annotations
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	if !reflect.DeepEqual(to.Spec.Replicas, from.Spec.Replicas) {
 		log.Info("Updating Zk replicas")
@@ -108,15 +110,35 @@
 	to.Spec.Image.Tag = from.Spec.Image.Tag
 
 	if from.Spec.Persistence != nil {
-		if !reflect.DeepEqual(to.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests, from.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests) {
+		if to.Spec.Persistence == nil {
+			log.Info("Updating Zk Persistence")
 			requireUpdate = true
-		}
-		to.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests = from.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests
+			to.Spec.Persistence = from.Spec.Persistence
+		} else {
+			if !reflect.DeepEqual(to.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests, from.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests) {
+				log.Info("Updating Zk Persistence PVC Requests")
+				requireUpdate = true
+				to.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests = from.Spec.Persistence.PersistentVolumeClaimSpec.Resources.Requests
+			}
 
-		if !reflect.DeepEqual(to.Spec.Persistence.VolumeReclaimPolicy, from.Spec.Persistence.VolumeReclaimPolicy) {
-			requireUpdate = true
+			if !reflect.DeepEqual(to.Spec.Persistence.PersistentVolumeClaimSpec.AccessModes, from.Spec.Persistence.PersistentVolumeClaimSpec.AccessModes) {
+				log.Info("Updating Zk Persistence PVC AccessModes")
+				requireUpdate = true
+				to.Spec.Persistence.PersistentVolumeClaimSpec.AccessModes = from.Spec.Persistence.PersistentVolumeClaimSpec.AccessModes
+			}
+
+			if !reflect.DeepEqual(to.Spec.Persistence.PersistentVolumeClaimSpec.StorageClassName, from.Spec.Persistence.PersistentVolumeClaimSpec.StorageClassName) {
+				log.Info("Updating Zk Persistence PVC StorageClassName")
+				requireUpdate = true
+				to.Spec.Persistence.PersistentVolumeClaimSpec.StorageClassName = from.Spec.Persistence.PersistentVolumeClaimSpec.StorageClassName
+			}
+
+			if !reflect.DeepEqual(to.Spec.Persistence.VolumeReclaimPolicy, from.Spec.Persistence.VolumeReclaimPolicy) {
+				log.Info("Updating Zk Persistence VolumeReclaimPolicy")
+				requireUpdate = true
+				to.Spec.Persistence.VolumeReclaimPolicy = from.Spec.Persistence.VolumeReclaimPolicy
+			}
 		}
-		to.Spec.Persistence.VolumeReclaimPolicy = from.Spec.Persistence.VolumeReclaimPolicy
 	}
 	/* Uncomment when the following PR is merged in: https://github.com/pravega/zookeeper-operator/pull/64
 	   Otherwise the ZK Operator will create persistence when none is given, and this will infinitely loop.
@@ -190,20 +212,7 @@
 // CopyEtcdClusterFields copies the owned fields from one EtcdCluster to another
 // Returns true if the fields copied from don't match to.
 func CopyEtcdClusterFields(from, to *etcd.EtcdCluster) bool {
-	requireUpdate := false
-	for k, v := range to.Labels {
-		if from.Labels[k] != v {
-			requireUpdate = true
-		}
-	}
-	to.Labels = from.Labels
-
-	for k, v := range to.Annotations {
-		if from.Annotations[k] != v {
-			requireUpdate = true
-		}
-	}
-	to.Annotations = from.Annotations
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	if !reflect.DeepEqual(to.Spec, from.Spec) {
 		requireUpdate = true
@@ -268,20 +277,7 @@
 // CopyDeploymentFields copies the owned fields from one Deployment to another
 // Returns true if the fields copied from don't match to.
 func CopyDeploymentFields(from, to *appsv1.Deployment) bool {
-	requireUpdate := false
-	for k, v := range from.Labels {
-		if to.Labels[k] != v {
-			requireUpdate = true
-		}
-		to.Labels[k] = v
-	}
-
-	for k, v := range from.Annotations {
-		if to.Annotations[k] != v {
-			requireUpdate = true
-		}
-		to.Annotations[k] = v
-	}
+	requireUpdate := CopyLabelsAndAnnotations(&from.ObjectMeta, &to.ObjectMeta)
 
 	if !reflect.DeepEqual(to.Spec.Replicas, from.Spec.Replicas) {
 		requireUpdate = true
@@ -382,3 +378,40 @@
 	}
 	return service
 }
+
+func CreateChRootIfNecessary(info solr.ZookeeperConnectionInfo) error {
+	if info.InternalConnectionString != "" && info.ChRoot != "/" {
+		zkClient, _, err := zkCli.Connect(strings.Split(info.InternalConnectionString, ","), time.Second)
+		if err != nil {
+			log.Error(err, "Could not connect to Zookeeper", "connectionString", info.InternalConnectionString)
+			return err
+		}
+
+		pathParts := strings.Split(strings.TrimPrefix(info.ChRoot, "/"), "/")
+		pathToCreate := ""
+		// Loop through each parent of the ZNode, and make sure that they exist recursively
+		for _, part := range pathParts {
+			if part == "" {
+				continue
+			}
+			pathToCreate += "/" + part
+
+			// Make sure that this part of the chRoot exists
+			exists, _, err := zkClient.Exists(pathToCreate)
+			if err != nil {
+				log.Error(err, "Could not check existence of Znode", "path", pathToCreate)
+				return err
+			} else if !exists {
+				log.Info("Creating Znode for chRoot of SolrCloud", "path", pathToCreate)
+				_, err = zkClient.Create(pathToCreate, []byte(""), 0, zkCli.WorldACL(zkCli.PermAll))
+
+				if err != nil {
+					log.Error(err, "Could not create Znode for chRoot of SolrCloud", "path", pathToCreate)
+					return err
+				}
+			}
+		}
+		return err
+	}
+	return nil
+}
diff --git a/example/test_solrcloud.yaml b/example/test_solrcloud.yaml
index 5057d0c..5768ab7 100644
--- a/example/test_solrcloud.yaml
+++ b/example/test_solrcloud.yaml
@@ -23,6 +23,7 @@
         memory: "156Mi"
   zookeeperRef:
     provided:
+      chroot: "/this/will/be/auto/created"
       zookeeper:
         persistence:
           spec:
diff --git a/go.mod b/go.mod
index 39633e1..965db66 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@
 	github.com/onsi/ginkgo v1.8.0 // indirect
 	github.com/onsi/gomega v1.5.0
 	github.com/pravega/zookeeper-operator v0.2.5-rc0
+	github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da
 	github.com/spf13/pflag v1.0.3 // indirect
 	github.com/stretchr/testify v1.3.0
 	golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8 // indirect
diff --git a/go.sum b/go.sum
index a092415..579c4d2 100644
--- a/go.sum
+++ b/go.sum
@@ -52,6 +52,7 @@
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4 h1:bRzFpEzvausOAt4va+I/22BZ1vXDtERngp0BNYDKej0=
 github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
 github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -208,6 +209,8 @@
 github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M=
 github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da h1:p3Vo3i64TCLY7gIfzeQaUJ+kppEO5WQG3cL8iE8tGHU=
+github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E=
 github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
 github.com/soheilhy/cmux v0.1.3/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
 github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=