From 034a086f9bc509218c29fce391509dafa571ff81 Mon Sep 17 00:00:00 2001
From: runkecheng <1131648942@qq.com>
Date: Thu, 5 Aug 2021 15:55:55 +0800
Subject: [PATCH] *: Install serviceMonitor when installing the operator #169.
 Automatically create a new monitoring service when the metrics container is
 enabled #169

---
 charts/mysql-operator/templates/_helpers.tpl  | 13 +++++
 .../templates/servicemonitor.yaml             | 28 +++++++++
 charts/mysql-operator/values.yaml             | 18 ++++++
 cluster/cluster.go                            |  2 +
 cluster/syncer/headless_service.go            | 19 ++-----
 cluster/syncer/metrics_service.go             | 57 +++++++++++++++++++
 controllers/cluster_controller.go             |  4 ++
 utils/constants.go                            |  2 +
 8 files changed, 130 insertions(+), 13 deletions(-)
 create mode 100644 charts/mysql-operator/templates/servicemonitor.yaml
 create mode 100644 cluster/syncer/metrics_service.go

diff --git a/charts/mysql-operator/templates/_helpers.tpl b/charts/mysql-operator/templates/_helpers.tpl
index 1486c0ce..51fad245 100644
--- a/charts/mysql-operator/templates/_helpers.tpl
+++ b/charts/mysql-operator/templates/_helpers.tpl
@@ -40,3 +40,16 @@ Create the name of the service account to use
     {{ default "default" .Values.serviceAccount.name }}
 {{- end -}}
 {{- end -}}
+
+{{/*
+Common labels
+*/}}
+{{- define "mysql-operator.labels" -}}
+app.kubernetes.io/name: {{ include "mysql-operator.name" . }}
+helm.sh/chart: {{ include "mysql-operator.chart" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end -}}
diff --git a/charts/mysql-operator/templates/servicemonitor.yaml b/charts/mysql-operator/templates/servicemonitor.yaml
new file mode 100644
index 00000000..e23301b7
--- /dev/null
+++ b/charts/mysql-operator/templates/servicemonitor.yaml
@@ -0,0 +1,28 @@
+{{ if .Capabilities.APIVersions.Has "monitoring.coreos.com/v1" -}}{{ if .Values.serviceMonitor.enabled -}}
+apiVersion: monitoring.coreos.com/v1
+kind: ServiceMonitor
+metadata:
+  name: {{ template "mysql-operator.fullname" . }}
+  labels:
+    {{- include "mysql-operator.labels" . | nindent 4 }}
+    {{- if .Values.serviceMonitor.additionalLabels }}
+    {{ toYaml .Values.serviceMonitor.additionalLabels }}
+    {{- end }}
+spec:
+  {{- if .Values.serviceMonitor.jobLabel }}
+  jobLabel: {{ .Values.serviceMonitor.jobLabel }}
+  {{- end }}
+  {{- if .Values.serviceMonitor.targetLabels }}
+  targetLabels: {{ .Values.serviceMonitor.targetLabels }}
+  {{- end }}
+  {{- if .Values.serviceMonitor.podTargetLabels }}
+  podTargetLabels: {{ .Values.serivceMonitor.podTargetLabels }}
+  {{- end }}
+  endpoints:
+    - interval: {{ .Values.serviceMonitor.interval }}
+      scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }}
+      port: metrics
+      path: /metrics
+  namespaceSelector: {{ toYaml .Values.serviceMonitor.namespaceSelector | nindent 4 }}
+  selector: {{ toYaml .Values.serviceMonitor.selector | nindent 4 }}
+{{ end -}}{{ end -}}
diff --git a/charts/mysql-operator/values.yaml b/charts/mysql-operator/values.yaml
index 4c75ad47..2399b794 100644
--- a/charts/mysql-operator/values.yaml
+++ b/charts/mysql-operator/values.yaml
@@ -43,3 +43,21 @@ rbacProxy:
 
 leaderElection:
   create: true
+
+serviceMonitor:
+  enabled: true
+  ## Additional labels for the serviceMonitor. Useful if you have multiple prometheus operators running to select only specific ServiceMonitors
+  # additionalLabels:
+  #   prometheus: prom-internal
+  interval: 10s
+  scrapeTimeout: 3s
+  # jobLabel:
+  # targetLabels:
+  # podTargetLabels:
+  namespaceSelector:
+    any: true
+  selector:
+    matchLabels:
+      app.kubernetes.io/managed-by: mysql.radondb.com
+      app.kubernetes.io/name: mysql
+
diff --git a/cluster/cluster.go b/cluster/cluster.go
index d10d0f3c..30a4927d 100644
--- a/cluster/cluster.go
+++ b/cluster/cluster.go
@@ -260,6 +260,8 @@ func (c *Cluster) GetNameForResource(name utils.ResourceName) string {
 		return fmt.Sprintf("%s-leader", c.Name)
 	case utils.FollowerService:
 		return fmt.Sprintf("%s-follower", c.Name)
+	case utils.MetricsService:
+		return fmt.Sprintf("%s-metrics", c.Name)
 	case utils.Secret:
 		return fmt.Sprintf("%s-secret", c.Name)
 	default:
diff --git a/cluster/syncer/headless_service.go b/cluster/syncer/headless_service.go
index f70f8390..1cc146c5 100644
--- a/cluster/syncer/headless_service.go
+++ b/cluster/syncer/headless_service.go
@@ -56,21 +56,14 @@ func NewHeadlessSVCSyncer(cli client.Client, c *cluster.Cluster) syncer.Interfac
 		// Use `publishNotReadyAddresses` to be able to access pods even if the pod is not ready.
 		service.Spec.PublishNotReadyAddresses = true
 
-		service.Spec.Ports = []corev1.ServicePort{
-			{
-				Name:       utils.MysqlPortName,
-				Port:       utils.MysqlPort,
-				TargetPort: intstr.FromInt(utils.MysqlPort),
-			},
+		if len(service.Spec.Ports) != 1 {
+			service.Spec.Ports = make([]corev1.ServicePort, 1)
 		}
 
-		if c.Spec.MetricsOpts.Enabled {
-			service.Spec.Ports = append(service.Spec.Ports, corev1.ServicePort{
-				Name:       utils.MetricsPortName,
-				Port:       utils.MetricsPort,
-				TargetPort: intstr.FromInt(utils.MetricsPort),
-			})
-		}
+		service.Spec.Ports[0].Name = utils.MysqlPortName
+		service.Spec.Ports[0].Port = utils.MysqlPort
+		service.Spec.Ports[0].TargetPort = intstr.FromInt(utils.MysqlPort)
+
 		return nil
 	})
 }
diff --git a/cluster/syncer/metrics_service.go b/cluster/syncer/metrics_service.go
new file mode 100644
index 00000000..19cfdb1c
--- /dev/null
+++ b/cluster/syncer/metrics_service.go
@@ -0,0 +1,57 @@
+/*
+Copyright 2021 RadonDB.
+
+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 syncer
+
+import (
+	"github.com/presslabs/controller-util/syncer"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+
+	"github.com/radondb/radondb-mysql-kubernetes/cluster"
+	"github.com/radondb/radondb-mysql-kubernetes/utils"
+)
+
+// NewMetricsSVCSyncer returns metrics service syncer.
+func NewMetricsSVCSyncer(cli client.Client, c *cluster.Cluster) syncer.Interface {
+	service := &corev1.Service{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "Service",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      c.GetNameForResource(utils.MetricsService),
+			Namespace: c.Namespace,
+			Labels:    c.GetLabels(),
+		},
+	}
+	return syncer.NewObjectSyncer("MetricsSVC", c.Unwrap(), service, cli, func() error {
+		service.Spec.Type = "ClusterIP"
+		service.Spec.Selector = c.GetSelectorLabels()
+
+		if len(service.Spec.Ports) != 1 {
+			service.Spec.Ports = make([]corev1.ServicePort, 1)
+		}
+
+		service.Spec.Ports[0].Name = utils.MetricsPortName
+		service.Spec.Ports[0].Port = utils.MetricsPort
+		service.Spec.Ports[0].TargetPort = intstr.FromInt(utils.MetricsPort)
+		
+		return nil
+	})
+}
diff --git a/controllers/cluster_controller.go b/controllers/cluster_controller.go
index ed378200..fba466ac 100644
--- a/controllers/cluster_controller.go
+++ b/controllers/cluster_controller.go
@@ -118,6 +118,10 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 		clustersyncer.NewPDBSyncer(r.Client, instance),
 	}
 
+	if instance.Spec.MetricsOpts.Enabled {
+		syncers = append(syncers, clustersyncer.NewMetricsSVCSyncer(r.Client, instance))
+	}
+
 	// run the syncers
 	for _, sync := range syncers {
 		if err = syncer.Sync(ctx, sync, r.Recorder); err != nil {
diff --git a/utils/constants.go b/utils/constants.go
index 731ee8e7..4bca925d 100644
--- a/utils/constants.go
+++ b/utils/constants.go
@@ -105,6 +105,8 @@ const (
 	LeaderService ResourceName = "leader-service"
 	// FollowerService is the name of a service that points healthy followers (excludes leader).
 	FollowerService ResourceName = "follower-service"
+	//MetricsService is the name of the metrics service that points to all nodes.
+	MetricsService ResourceName = "metrics-service"
 	// Secret is the name of the secret that contains operator related credentials.
 	Secret ResourceName = "secret"
 	// Role is the alias of the role resource.