Repository: pravega/zookeeper-operator (go)

Issue:
Expose ZooKeeper AdminServer port via Service
### Description

ZooKeeper 3.5.0+ provides an AdminService via HTTP on port 8080 by default: https://zookeeper.apache.org/doc/r3.5.5/zookeeperAdmin.html#sc_adminserver.
It would useful to expose it to the outside world via the Kubernetes service.

### Importance

Should have.

### Location

I make a guess, but I suppose the changes must take place in the code at https://github.com/pravega/zookeeper-operator/blob/master/pkg/zk/generators.go#L260. Right?

Pull request description:
Issue 237: Exposing Zookeeper AdminServer port via Service
Signed-off-by: SrishT <Srishti.Thakkar@dell.com>

### Change log description
ZooKeeper 3.5.0+ provides an AdminService via HTTP on port 8080 by default, and this PR exposes it to the outside world via the Kubernetes service.

### Purpose of the change
Fixes #237 

### How to verify it
Deploying the zookeeper cluster also brings up the admin-server service, which can be seen with the following command
```
$ kubectl get svc
NAME                                TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)
zookeeper-admin-server              LoadBalancer   10.100.200.104   10.243.39.62   8080:30477/TCP 
```
This service can be accessed from outside the cluster in the following manner
```
$ curl -X GET "http://10.243.39.62:8080/commands/stat" -H "accept: application/json"
{
  "version" : "3.6.1--104dcb3e3fb464b30c5186d229e00af9f332524b, built on 04/21/2020 15:01 GMT",
  "read_only" : false,
  "server_stats" : {
    "packets_sent" : 8,
    "packets_received" : 8,
    "fsync_threshold_exceed_count" : 0,
    "client_response_stats" : {
      "last_buffer_size" : -1,
      "min_buffer_size" : -1,
      "max_buffer_size" : -1
    },
    "provider_null" : false,
    "uptime" : 43252,
    "server_state" : "follower",
    "outstanding_requests" : 0,
    "min_latency" : 0,
    "avg_latency" : 0.0,
    "max_latency" : 0,
    "data_dir_size" : 67110446,
    "log_dir_size" : 67110446,
    "last_processed_zxid" : 4294967322,
    "num_alive_client_connections" : 0
  },
  "client_response" : {
    "last_buffer_size" : -1,
    "min_buffer_size" : -1,
    "max_buffer_size" : -1
  },
  "node_count" : 7,
  "connections" : [ ],
  "secure_connections" : [ ],
  "command" : "stats",
  "error" : null
}
```
The following is the list of commands that can be accessed from the admin-server port
```
/commands/configuration
/commands/connection_stat_reset
/commands/connections
/commands/dirs
/commands/dump
/commands/environment
/commands/get_trace_mask
/commands/hash
/commands/initial_configuration
/commands/is_read_only
/commands/last_snapshot
/commands/leader
/commands/monitor
/commands/observer_connection_stat_reset
/commands/observers
/commands/ruok
/commands/server_stats
/commands/set_trace_mask
/commands/stat_reset
/commands/stats
/commands/system_properties
/commands/voting_view
/commands/watch_summary
/commands/watches
/commands/watches_by_path
/commands/zabstate
```

Patch:
diff --git a/README.md b/README.md
index d2be551..f58dbc3 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@ The project is currently alpha. While no breaking API changes are currently plan
     * [Uninstall the Zookeeper Cluster](#uninstall-the-zookeeper-cluster)
     * [Upgrade the Zookeeper Operator](#upgrade-the-operator)
     * [Uninstall the Operator](#uninstall-the-operator)
+    * [The AdminServer](#the-adminserver)
  * [Development](#development)
     * [Build the Operator Image](#build-the-operator-image)
     * [Direct Access to Cluster](#direct-access-to-the-cluster)
@@ -315,6 +316,51 @@ $ kubectl delete -f deploy/default_ns
 $ kubectl delete -f deploy/all_ns
 ```
 
+### The AdminServer
+The AdminServer is an embedded Jetty server that provides an HTTP interface to the four letter word commands. This port is made accessible to the outside world via the AdminServer service.
+By default, the server is started on port 8080, but this configuration can be modified by providing the desired port number within the values.yaml file of the zookeeper cluster charts
+```
+ports:
+   - containerPort: 8118
+     name: admin-server
+```
+This would bring up the AdminServer service on port 8118 as shown below
+```
+$ kubectl get svc
+NAME                                TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)
+zookeeper-admin-server              LoadBalancer   10.100.200.104   10.243.39.62   8118:30477/TCP
+```
+The commands are issued by going to the URL `/commands/<command name>`, e.g. `http://10.243.39.62:8118/commands/stat`
+The list of available commands are
+```
+/commands/configuration
+/commands/connection_stat_reset
+/commands/connections
+/commands/dirs
+/commands/dump
+/commands/environment
+/commands/get_trace_mask
+/commands/hash
+/commands/initial_configuration
+/commands/is_read_only
+/commands/last_snapshot
+/commands/leader
+/commands/monitor
+/commands/observer_connection_stat_reset
+/commands/observers
+/commands/ruok
+/commands/server_stats
+/commands/set_trace_mask
+/commands/stat_reset
+/commands/stats
+/commands/system_properties
+/commands/voting_view
+/commands/watch_summary
+/commands/watches
+/commands/watches_by_path
+/commands/zabstate
+```
+
 ## Development
 
 ### Build the operator image
diff --git a/charts/zookeeper/values.yaml b/charts/zookeeper/values.yaml
index f6f2966..cab8daa 100644
--- a/charts/zookeeper/values.yaml
+++ b/charts/zookeeper/values.yaml
@@ -8,6 +8,12 @@ image:
 domainName:
 labels: {}
 ports: []
+  # - containerPort: 2181
+  #   name: client
+  # - containerPort: 2888
+  #   name: quorum
+  # - containerPort: 8080
+  #   name: admin-server
 kubernetesClusterDomain: "cluster.local"
 probes:
   readiness:
diff --git a/pkg/apis/zookeeper/v1beta1/zookeepercluster_types.go b/pkg/apis/zookeeper/v1beta1/zookeepercluster_types.go
index e92ab87..a06d657 100644
--- a/pkg/apis/zookeeper/v1beta1/zookeepercluster_types.go
+++ b/pkg/apis/zookeeper/v1beta1/zookeepercluster_types.go
@@ -178,11 +178,15 @@ func (s *ZookeeperClusterSpec) withDefaults(z *ZookeeperCluster) (changed bool)
 				Name:          "metrics",
 				ContainerPort: 7000,
 			},
+			{
+				Name:          "admin-server",
+				ContainerPort: 8080,
+			},
 		}
 		changed = true
 	} else {
 		var (
-			foundClient, foundQuorum, foundLeader, foundMetrics bool
+			foundClient, foundQuorum, foundLeader, foundMetrics, foundAdmin bool
 		)
 		for i := 0; i < len(s.Ports); i++ {
 			if s.Ports[i].Name == "client" {
@@ -193,6 +197,8 @@ func (s *ZookeeperClusterSpec) withDefaults(z *ZookeeperCluster) (changed bool)
 				foundLeader = true
 			} else if s.Ports[i].Name == "metrics" {
 				foundMetrics = true
+			} else if s.Ports[i].Name == "admin-server" {
+				foundAdmin = true
 			}
 		}
 		if !foundClient {
@@ -215,6 +221,11 @@ func (s *ZookeeperClusterSpec) withDefaults(z *ZookeeperCluster) (changed bool)
 			s.Ports = append(s.Ports, ports)
 			changed = true
 		}
+		if !foundAdmin {
+			ports := v1.ContainerPort{Name: "admin-server", ContainerPort: 8080}
+			s.Ports = append(s.Ports, ports)
+			changed = true
+		}
 	}
 
 	if z.Spec.Labels == nil {
@@ -323,6 +334,8 @@ func (z *ZookeeperCluster) ZookeeperPorts() Ports {
 			ports.Leader = p.ContainerPort
 		} else if p.Name == "metrics" {
 			ports.Metrics = p.ContainerPort
+		} else if p.Name == "admin-server" {
+			ports.AdminServer = p.ContainerPort
 		}
 	}
 	return ports
@@ -333,12 +346,18 @@ func (z *ZookeeperCluster) GetClientServiceName() string {
 	return fmt.Sprintf("%s-client", z.GetName())
 }
 
+// GetAdminServerServiceName returns the name of the admin server service for the cluster
+func (z *ZookeeperCluster) GetAdminServerServiceName() string {
+	return fmt.Sprintf("%s-admin-server", z.GetName())
+}
+
 // Ports groups the ports for a zookeeper cluster node for easy access
 type Ports struct {
-	Client  int32
-	Quorum  int32
-	Leader  int32
-	Metrics int32
+	Client      int32
+	Quorum      int32
+	Leader      int32
+	Metrics     int32
+	AdminServer int32
 }
 
 // ContainerImage defines the fields needed for a Docker repository image. The
diff --git a/pkg/controller/zookeepercluster/zookeepercluster_controller.go b/pkg/controller/zookeepercluster/zookeepercluster_controller.go
index 39e869d..1a154aa 100644
--- a/pkg/controller/zookeepercluster/zookeepercluster_controller.go
+++ b/pkg/controller/zookeepercluster/zookeepercluster_controller.go
@@ -152,6 +152,7 @@ func (r *ReconcileZookeeperCluster) Reconcile(request reconcile.Request) (reconc
 		r.reconcileStatefulSet,
 		r.reconcileClientService,
 		r.reconcileHeadlessService,
+		r.reconcileAdminServerService,
 		r.reconcilePodDisruptionBudget,
 		r.reconcileClusterStatus,
 	} {
@@ -419,6 +420,40 @@ func (r *ReconcileZookeeperCluster) reconcileHeadlessService(instance *zookeeper
 	return nil
 }
 
+func (r *ReconcileZookeeperCluster) reconcileAdminServerService(instance *zookeeperv1beta1.ZookeeperCluster) (err error) {
+	svc := zk.MakeAdminServerService(instance)
+	if err = controllerutil.SetControllerReference(instance, svc, r.scheme); err != nil {
+		return err
+	}
+	foundSvc := &corev1.Service{}
+	err = r.client.Get(context.TODO(), types.NamespacedName{
+		Name:      svc.Name,
+		Namespace: svc.Namespace,
+	}, foundSvc)
+	if err != nil && errors.IsNotFound(err) {
+		r.log.Info("Creating admin server service",
+			"Service.Namespace", svc.Namespace,
+			"Service.Name", svc.Name)
+		err = r.client.Create(context.TODO(), svc)
+		if err != nil {
+			return err
+		}
+		return nil
+	} else if err != nil {
+		return err
+	} else {
+		r.log.Info("Updating existing admin server service",
+			"Service.Namespace", foundSvc.Namespace,
+			"Service.Name", foundSvc.Name)
+		zk.SyncService(foundSvc, svc)
+		err = r.client.Update(context.TODO(), foundSvc)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
 func (r *ReconcileZookeeperCluster) reconcilePodDisruptionBudget(instance *zookeeperv1beta1.ZookeeperCluster) (err error) {
 	pdb := zk.MakePodDisruptionBudget(instance)
 	if err = controllerutil.SetControllerReference(instance, pdb, r.scheme); err != nil {
diff --git a/pkg/zk/generators.go b/pkg/zk/generators.go
index 13cee68..3f6f8ba 100644
--- a/pkg/zk/generators.go
+++ b/pkg/zk/generators.go
@@ -191,7 +191,17 @@ func MakeClientService(z *v1beta1.ZookeeperCluster) *v1.Service {
 	svcPorts := []v1.ServicePort{
 		{Name: "tcp-client", Port: ports.Client},
 	}
-	return makeService(z.GetClientServiceName(), svcPorts, true, z)
+	return makeService(z.GetClientServiceName(), svcPorts, true, false, z)
+}
+
+// MakeAdminServerService returns a service which provides an interface
+// to access the zookeeper admin server port
+func MakeAdminServerService(z *v1beta1.ZookeeperCluster) *v1.Service {
+	ports := z.ZookeeperPorts()
+	svcPorts := []v1.ServicePort{
+		{Name: "tcp-admin-server", Port: ports.AdminServer},
+	}
+	return makeService(z.GetAdminServerServiceName(), svcPorts, true, true, z)
 }
 
 // MakeConfigMap returns a zookeeper config map
@@ -207,7 +217,7 @@ func MakeConfigMap(z *v1beta1.ZookeeperCluster) *v1.ConfigMap {
 			Labels:    z.Spec.Labels,
 		},
 		Data: map[string]string{
-			"zoo.cfg":                makeZkConfigString(z.Spec),
+			"zoo.cfg":                makeZkConfigString(z),
 			"log4j.properties":       makeZkLog4JConfigString(),
 			"log4j-quiet.properties": makeZkLog4JQuietConfigString(),
 			"env.sh":                 makeZkEnvConfigString(z),
@@ -224,11 +234,13 @@ func MakeHeadlessService(z *v1beta1.ZookeeperCluster) *v1.Service {
 		{Name: "tcp-quorum", Port: ports.Quorum},
 		{Name: "tcp-leader-election", Port: ports.Leader},
 		{Name: "tcp-metrics", Port: ports.Metrics},
+		{Name: "tcp-admin-server", Port: ports.AdminServer},
 	}
-	return makeService(headlessSvcName(z), svcPorts, false, z)
+	return makeService(headlessSvcName(z), svcPorts, false, false, z)
 }
 
-func makeZkConfigString(s v1beta1.ZookeeperClusterSpec) string {
+func makeZkConfigString(z *v1beta1.ZookeeperCluster) string {
+	ports := z.ZookeeperPorts()
 	return "4lw.commands.whitelist=cons, envi, conf, crst, srvr, stat, mntr, ruok\n" +
 		"dataDir=/data\n" +
 		"standaloneEnabled=false\n" +
@@ -237,21 +249,22 @@ func makeZkConfigString(s v1beta1.ZookeeperClusterSpec) string {
 		"metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider\n" +
 		"metricsProvider.httpPort=7000\n" +
 		"metricsProvider.exportJvmInfo=true\n" +
-		"initLimit=" + strconv.Itoa(s.Conf.InitLimit) + "\n" +
-		"syncLimit=" + strconv.Itoa(s.Conf.SyncLimit) + "\n" +
-		"tickTime=" + strconv.Itoa(s.Conf.TickTime) + "\n" +
-		"globalOutstandingLimit=" + strconv.Itoa(s.Conf.GlobalOutstandingLimit) + "\n" +
-		"preAllocSize=" + strconv.Itoa(s.Conf.PreAllocSize) + "\n" +
-		"snapCount=" + strconv.Itoa(s.Conf.SnapCount) + "\n" +
-		"commitLogCount=" + strconv.Itoa(s.Conf.CommitLogCount) + "\n" +
-		"snapSizeLimitInKb=" + strconv.Itoa(s.Conf.SnapSizeLimitInKb) + "\n" +
-		"maxCnxns=" + strconv.Itoa(s.Conf.MaxCnxns) + "\n" +
-		"maxClientCnxns=" + strconv.Itoa(s.Conf.MaxClientCnxns) + "\n" +
-		"minSessionTimeout=" + strconv.Itoa(s.Conf.MinSessionTimeout) + "\n" +
-		"maxSessionTimeout=" + strconv.Itoa(s.Conf.MaxSessionTimeout) + "\n" +
-		"autopurge.snapRetainCount=" + strconv.Itoa(s.Conf.AutoPurgeSnapRetainCount) + "\n" +
-		"autopurge.purgeInterval=" + strconv.Itoa(s.Conf.AutoPurgePurgeInterval) + "\n" +
-		"quorumListenOnAllIPs=" + strconv.FormatBool(s.Conf.QuorumListenOnAllIPs) + "\n" +
+		"initLimit=" + strconv.Itoa(z.Spec.Conf.InitLimit) + "\n" +
+		"syncLimit=" + strconv.Itoa(z.Spec.Conf.SyncLimit) + "\n" +
+		"tickTime=" + strconv.Itoa(z.Spec.Conf.TickTime) + "\n" +
+		"globalOutstandingLimit=" + strconv.Itoa(z.Spec.Conf.GlobalOutstandingLimit) + "\n" +
+		"preAllocSize=" + strconv.Itoa(z.Spec.Conf.PreAllocSize) + "\n" +
+		"snapCount=" + strconv.Itoa(z.Spec.Conf.SnapCount) + "\n" +
+		"commitLogCount=" + strconv.Itoa(z.Spec.Conf.CommitLogCount) + "\n" +
+		"snapSizeLimitInKb=" + strconv.Itoa(z.Spec.Conf.SnapSizeLimitInKb) + "\n" +
+		"maxCnxns=" + strconv.Itoa(z.Spec.Conf.MaxCnxns) + "\n" +
+		"maxClientCnxns=" + strconv.Itoa(z.Spec.Conf.MaxClientCnxns) + "\n" +
+		"minSessionTimeout=" + strconv.Itoa(z.Spec.Conf.MinSessionTimeout) + "\n" +
+		"maxSessionTimeout=" + strconv.Itoa(z.Spec.Conf.MaxSessionTimeout) + "\n" +
+		"autopurge.snapRetainCount=" + strconv.Itoa(z.Spec.Conf.AutoPurgeSnapRetainCount) + "\n" +
+		"autopurge.purgeInterval=" + strconv.Itoa(z.Spec.Conf.AutoPurgePurgeInterval) + "\n" +
+		"quorumListenOnAllIPs=" + strconv.FormatBool(z.Spec.Conf.QuorumListenOnAllIPs) + "\n" +
+		"admin.serverPort=" + strconv.Itoa(int(ports.AdminServer)) + "\n" +
 		"dynamicConfigFile=/data/zoo.cfg.dynamic\n"
 }
 
@@ -281,11 +294,13 @@ func makeZkEnvConfigString(z *v1beta1.ZookeeperCluster) string {
 		"LEADER_PORT=" + strconv.Itoa(int(ports.Leader)) + "\n" +
 		"CLIENT_HOST=" + z.GetClientServiceName() + "\n" +
 		"CLIENT_PORT=" + strconv.Itoa(int(ports.Client)) + "\n" +
+		"ADMIN_SERVER_HOST=" + z.GetAdminServerServiceName() + "\n" +
+		"ADMIN_SERVER_PORT=" + strconv.Itoa(int(ports.AdminServer)) + "\n" +
 		"CLUSTER_NAME=" + z.GetName() + "\n" +
 		"CLUSTER_SIZE=" + fmt.Sprint(z.Spec.Replicas) + "\n"
 }
 
-func makeService(name string, ports []v1.ServicePort, clusterIP bool, z *v1beta1.ZookeeperCluster) *v1.Service {
+func makeService(name string, ports []v1.ServicePort, clusterIP bool, external bool, z *v1beta1.ZookeeperCluster) *v1.Service {
 	var dnsName string
 	var annotationMap map[string]string
 	if !clusterIP && z.Spec.DomainName != "" {
@@ -318,6 +333,9 @@ func makeService(name string, ports []v1.ServicePort, clusterIP bool, z *v1beta1
 			Selector: map[string]string{"app": z.GetName()},
 		},
 	}
+	if external {
+		service.Spec.Type = "LoadBalancer"
+	}
 	if !clusterIP {
 		service.Spec.ClusterIP = v1.ClusterIPNone
 	}


Test patch:
diff --git a/pkg/apis/zookeeper/v1beta1/zookeepercluster_types_test.go b/pkg/apis/zookeeper/v1beta1/zookeepercluster_types_test.go
index cc95652..f4e5dcd 100644
--- a/pkg/apis/zookeeper/v1beta1/zookeepercluster_types_test.go
+++ b/pkg/apis/zookeeper/v1beta1/zookeepercluster_types_test.go
@@ -127,10 +127,14 @@ var _ = Describe("ZookeeperCluster Types", func() {
 				Ω(z.GetKubernetesClusterDomain()).To(Equal("cluster.local"))
 			})
 
-			It("should give clientservicename as example-client", func() {
+			It("should give client service name as example-client", func() {
 				Ω(z.GetClientServiceName()).To(Equal("example-client"))
 			})
 
+			It("should give admin-server service name as example-admin-server", func() {
+				Ω(z.GetAdminServerServiceName()).To(Equal("example-admin-server"))
+			})
+
 			It("should set InitLimit to 10", func() {
 				Ω(c.InitLimit).To(Equal(10))
 			})
@@ -187,6 +191,20 @@ var _ = Describe("ZookeeperCluster Types", func() {
 				}))
 			})
 
+			It("should have the default metrics port", func() {
+				Ω(ports).To(ContainElement(corev1.ContainerPort{
+					Name:          "metrics",
+					ContainerPort: 7000,
+				}))
+			})
+
+			It("should have the default admin-server port", func() {
+				Ω(ports).To(ContainElement(corev1.ContainerPort{
+					Name:          "admin-server",
+					ContainerPort: 8080,
+				}))
+			})
+
 		})
 
 		Context("Pod Policy", func() {
@@ -277,6 +295,10 @@ var _ = Describe("ZookeeperCluster Types", func() {
 		It("should have a metrics port", func() {
 			Ω(p.Metrics).To(BeEquivalentTo(7000))
 		})
+
+		It("should have an admin-server port", func() {
+			Ω(p.AdminServer).To(BeEquivalentTo(8080))
+		})
 	})
 	Context("#ZookeeperPorts with values set", func() {
 		var p v1beta1.Ports
@@ -313,5 +335,9 @@ var _ = Describe("ZookeeperCluster Types", func() {
 		It("should have a metrics port", func() {
 			Ω(p.Metrics).To(BeEquivalentTo(7000))
 		})
+
+		It("should have an admin-server port", func() {
+			Ω(p.AdminServer).To(BeEquivalentTo(8080))
+		})
 	})
 })
diff --git a/pkg/zk/generators_test.go b/pkg/zk/generators_test.go
index d2fb1c6..8afee47 100644
--- a/pkg/zk/generators_test.go
+++ b/pkg/zk/generators_test.go
@@ -125,6 +125,14 @@ var _ = Describe("Generators Spec", func() {
 					Ω(cfg).To(ContainSubstring("CLIENT_PORT=2181\n"))
 				})
 
+				It("should set the ADMIN_SERVER_HOST", func() {
+					Ω(cfg).To(ContainSubstring("ADMIN_SERVER_HOST=example-admin-server\n"))
+				})
+
+				It("should set the ADMIN_SERVER_PORT", func() {
+					Ω(cfg).To(ContainSubstring("ADMIN_SERVER_PORT=8080\n"))
+				})
+
 				It("should set the LEADER_PORT", func() {
 					Ω(cfg).To(ContainSubstring("LEADER_PORT=3888\n"))
 				})
@@ -301,11 +309,11 @@ var _ = Describe("Generators Spec", func() {
 			Ω(p.Port).To(BeEquivalentTo(2181))
 		})
 
-		It("should have a the client svc name", func() {
+		It("should have a client svc name", func() {
 			Ω(s.GetName()).To(Equal("example-client"))
 		})
 
-		It("should have a the client svc name", func() {
+		It("should have a client svc name", func() {
 			Ω(s.Spec.Selector["app"]).To(Equal("example"))
 		})
 
@@ -367,11 +375,11 @@ var _ = Describe("Generators Spec", func() {
 			Ω(p.Port).To(BeEquivalentTo(7000))
 		})
 
-		It("should have a the client svc name", func() {
+		It("should have a client svc name", func() {
 			Ω(s.GetName()).To(Equal("example-headless"))
 		})
 
-		It("should have a the client svc name", func() {
+		It("should have a client svc name", func() {
 			Ω(s.Spec.Selector["app"]).To(Equal("example"))
 		})
 
@@ -387,6 +395,7 @@ var _ = Describe("Generators Spec", func() {
 				"exampleValue"))
 		})
 	})
+
 	Context("#MakeHeadlessService dnsname without dot", func() {
 		var s *v1.Service
 		var domainName string
@@ -413,6 +422,46 @@ var _ = Describe("Generators Spec", func() {
 		})
 	})
 
+	Context("#MakeAdminServerService", func() {
+		var s *v1.Service
+
+		BeforeEach(func() {
+			z := &v1beta1.ZookeeperCluster{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "example",
+					Namespace: "default",
+				},
+				Spec: v1beta1.ZookeeperClusterSpec{
+					Labels: map[string]string{
+						"exampleLabel": "exampleValue",
+					},
+				},
+			}
+			z.WithDefaults()
+			s = zk.MakeAdminServerService(z)
+		})
+
+		It("should have an admin server port", func() {
+			p, err := utils.ServicePortByName(s.Spec.Ports, "tcp-admin-server")
+			Ω(err).To(BeNil())
+			Ω(p.Port).To(BeEquivalentTo(8080))
+		})
+
+		It("should have an admin server svc name", func() {
+			Ω(s.GetName()).To(Equal("example-admin-server"))
+		})
+
+		It("should have a client svc name", func() {
+			Ω(s.Spec.Selector["app"]).To(Equal("example"))
+		})
+
+		It("should have custom labels set", func() {
+			Ω(s.GetLabels()).To(HaveKeyWithValue(
+				"exampleLabel",
+				"exampleValue"))
+		})
+	})
+
 	Context("#MakePodDisruptionBudget", func() {
 		var pdb *policyv1beta1.PodDisruptionBudget
 		var domainName string
diff --git a/pkg/zk/synchronizers_test.go b/pkg/zk/synchronizers_test.go
index 0d9362e..5fce550 100644
--- a/pkg/zk/synchronizers_test.go
+++ b/pkg/zk/synchronizers_test.go
@@ -87,7 +87,7 @@ var _ = Describe("Synchronizers", func() {
 		})
 	})
 
-	Context("with a valid update of config map ", func() {
+	Context("with a valid update of config map", func() {
 		var value string
 		BeforeEach(func() {
 			z := &v1beta1.ZookeeperCluster{