horizoncd/horizon

View on GitHub
pkg/grafana/grafana.go

Summary

Maintainability
A
2 hrs
Test Coverage
// Copyright © 2023 Horizoncd.
//
// 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 grafana
 
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"time"
 
herrors "github.com/horizoncd/horizon/core/errors"
"github.com/horizoncd/horizon/pkg/config/grafana"
perror "github.com/horizoncd/horizon/pkg/errors"
"github.com/horizoncd/horizon/pkg/param/managerparam"
regionmanager "github.com/horizoncd/horizon/pkg/region/manager"
"github.com/horizoncd/horizon/pkg/util/log"
"gopkg.in/yaml.v3"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
 
const (
_datasourceConfigMapName = "grafana-datasource"
_prometheusDatasourceType = "prometheus"
_contentMD5AnnotationKey = "content-md5"
_datasourceDataKey = "horizon-datasource.yaml"
_datasourceAPIVersion = 1
)
 
type Service interface {
SyncDatasource(ctx context.Context)
ListDashboards(ctx context.Context) ([]*Dashboard, error)
}
 
type service struct {
config grafana.Config
kubeClient kubernetes.Interface
regionMgr regionmanager.Manager
}
 
func NewService(config grafana.Config, manager *managerparam.Manager, client kubernetes.Interface) Service {
return &service{
config: config,
kubeClient: client,
regionMgr: manager.RegionMgr,
}
}
 
type Content struct {
APIVersion int `yaml:"apiVersion"`
Datasources []DataSource `yaml:"datasources"`
}
 
type DataSource struct {
Name string `yaml:"name"`
Type string `yaml:"type"` // only use prometheus currently
URL string `yaml:"url"`
}
 
// Dashboard used to unmarshal grafana dashboard's content
type Dashboard struct {
Title string `json:"title"`
UID string `json:"uid"`
Tags []string `json:"tags"`
}
 
func (s *service) SyncDatasource(ctx context.Context) {
log.Infof(ctx, "Starting syncing grafana datasource every %v", s.config.SyncDatasourceConfig.Period)
defer log.Infof(ctx, "Stopping syncing grafana datasource")
 
ticker := time.NewTicker(s.config.SyncDatasourceConfig.Period)
defer ticker.Stop()
 
for {
select {
case <-ctx.Done():
log.Debug(ctx, "Get done signal from context")
return
case <-ticker.C:
s.sync(ctx)
}
}
}
 
Method `service.sync` has 72 lines of code (exceeds 50 allowed). Consider refactoring.
Method `service.sync` has 5 return statements (exceeds 4 allowed).
func (s *service) sync(ctx context.Context) {
log.Info(ctx, "Start to sync grafana datasource")
 
logErr := func(err error) {
log.Errorf(ctx, "Sync grafana datasource error: %+v", err)
}
 
regions, err := s.regionMgr.ListAll(ctx)
if err != nil {
logErr(err)
return
}
 
configMapOps := s.kubeClient.CoreV1().ConfigMaps(s.config.Namespace)
datasourceConfigMap, err := configMapOps.Get(ctx, _datasourceConfigMapName, metav1.GetOptions{})
if err != nil {
if statusError, ok := err.(*k8serrors.StatusError); !ok || statusError.ErrStatus.Code != http.StatusNotFound {
logErr(err)
return
}
}
 
var datasources []DataSource
for _, region := range regions {
datasourceURL := region.PrometheusURL
datasources = append(datasources, DataSource{
Name: region.Name,
Type: _prometheusDatasourceType,
URL: datasourceURL,
})
}
 
content := Content{
APIVersion: _datasourceAPIVersion,
Datasources: datasources,
}
dsBytes, err := yaml.Marshal(&content)
if err != nil {
logErr(err)
return
}
h := md5.New()
h.Write(dsBytes)
curMD5Val := hex.EncodeToString(h.Sum(nil))
curConfigmap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: _datasourceConfigMapName,
Labels: map[string]string{
s.config.SyncDatasourceConfig.LabelKey: s.config.SyncDatasourceConfig.LabelValue,
},
Annotations: map[string]string{
_contentMD5AnnotationKey: curMD5Val,
},
},
Data: map[string]string{
_datasourceDataKey: string(dsBytes),
},
}
 
// datasourceConfigMap is not nil though it is a pointer because the client-go's logic
contentMD5, ok := datasourceConfigMap.ObjectMeta.Annotations[_contentMD5AnnotationKey]
if !ok {
// create configmap
_, err := configMapOps.Create(ctx, curConfigmap, metav1.CreateOptions{})
if err != nil {
logErr(err)
} else {
log.Infof(ctx, "Create grafana datasource successfully, content: %s", string(dsBytes))
}
return
}
 
// update the configmap if md5 values are not equal.
if contentMD5 != curMD5Val {
_, err := configMapOps.Update(ctx, curConfigmap, metav1.UpdateOptions{})
if err != nil {
logErr(err)
} else {
log.Infof(ctx, "Update grafana datasource successfully, content: %s", string(dsBytes))
}
return
}
 
log.Debug(ctx, "Skip updating datasource because there are no changes")
}
 
func (s *service) ListDashboards(ctx context.Context) ([]*Dashboard, error) {
configMapOps := s.kubeClient.CoreV1().ConfigMaps(s.config.Namespace)
 
dashboardConfigMapList, err := configMapOps.List(ctx,
metav1.ListOptions{
LabelSelector: fmt.Sprintf("%v=%v", s.config.Dashboards.LabelKey,
s.config.Dashboards.LabelValue),
})
if err != nil {
if statusError, ok := err.(*k8serrors.StatusError); !ok || statusError.ErrStatus.Code != http.StatusNotFound {
return nil, perror.Wrap(herrors.ErrListGrafanaDashboard, err.Error())
}
}
 
var dashboards []*Dashboard
if dashboardConfigMapList != nil {
for _, item := range dashboardConfigMapList.Items {
for _, val := range item.Data {
var dashboard Dashboard
err = json.Unmarshal([]byte(val), &dashboard)
if err != nil {
return nil, perror.WithMessage(herrors.ErrListGrafanaDashboard, err.Error())
}
dashboards = append(dashboards, &dashboard)
}
}
}
 
return dashboards, nil
}