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=