Skip to content

Commit 5175b26

Browse files
gnunn1wtam2018
andauthored
Add OpenShift Dashboards for GitOps (redhat-developer#589)
* Provision dashboards for OpenShift Console * Create constant for dashboards folder path * Add unit test * Improve error message when dashboard namespace is not available * Remove redis graph due to upstream bug not exposing stats * Update function name and message as per Jaideep's review comments * Reconcile dashboards if data in configmap changes * Change name to reconcileDashboards since we are now doing reconciliation and not just create * Continue processing dashboards if error occurs instead of stopping * Continue processing dashboards if new dashboard creation fails --------- Co-authored-by: William Tam <[email protected]>
1 parent ae03a0b commit 5175b26

5 files changed

+3607
-0
lines changed

controllers/argocd_metrics_controller.go

+101
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ package controllers
1818

1919
import (
2020
"context"
21+
"embed"
2122
"fmt"
23+
"path/filepath"
24+
"strings"
2225

2326
argoapp "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
2427
monitoringv1 "github.com/coreos/prometheus-operator/pkg/apis/monitoring/v1"
@@ -41,6 +44,8 @@ const (
4144
readRoleNameFormat = "%s-read"
4245
readRoleBindingNameFormat = "%s-prometheus-k8s-read-binding"
4346
alertRuleName = "gitops-operator-argocd-alerts"
47+
dashboardNamespace = "openshift-config-managed"
48+
dashboardFolder = "dashboards"
4449
)
4550

4651
type ArgoCDMetricsReconciler struct {
@@ -50,6 +55,12 @@ type ArgoCDMetricsReconciler struct {
5055
Scheme *runtime.Scheme
5156
}
5257

58+
// embed json dashboards
59+
var (
60+
//go:embed dashboards
61+
dashboards embed.FS
62+
)
63+
5364
// blank assignment to verify that ReconcileArgoCDRoute implements reconcile.Reconciler
5465
var _ reconcile.Reconciler = &ArgoCDMetricsReconciler{}
5566

@@ -154,6 +165,11 @@ func (r *ArgoCDMetricsReconciler) Reconcile(ctx context.Context, request reconci
154165
return reconcile.Result{}, err
155166
}
156167

168+
err = r.reconcileDashboards(reqLogger)
169+
if err != nil {
170+
return reconcile.Result{}, err
171+
}
172+
157173
return reconcile.Result{}, nil
158174
}
159175

@@ -292,6 +308,91 @@ func (r *ArgoCDMetricsReconciler) createPrometheusRuleIfAbsent(namespace string,
292308
return err
293309
}
294310

311+
func (r *ArgoCDMetricsReconciler) reconcileDashboards(reqLogger logr.Logger) error {
312+
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: dashboardNamespace}, &corev1.Namespace{})
313+
if err != nil {
314+
reqLogger.Info("Monitoring dashboards are not supported on this cluster, skipping dashboard installation",
315+
"Namespace", dashboardNamespace)
316+
return nil
317+
}
318+
319+
entries, err := dashboards.ReadDir(dashboardFolder)
320+
if err != nil {
321+
reqLogger.Error(err, "Could not read list of embedded dashboards")
322+
return err
323+
}
324+
325+
for _, entry := range entries {
326+
reqLogger.Info("Processing dashboard", "Namespace", dashboardNamespace, "Name", entry.Name())
327+
328+
if !entry.IsDir() {
329+
dashboard, err := newDashboardConfigMap(entry.Name(), dashboardNamespace)
330+
if err != nil {
331+
reqLogger.Info("There was an error creating dashboard ", "Namespace", dashboardNamespace, "Name", entry.Name())
332+
continue
333+
}
334+
335+
existingDashboard := &corev1.ConfigMap{}
336+
337+
err = r.Client.Get(context.TODO(), types.NamespacedName{Name: dashboard.Name, Namespace: dashboardNamespace}, existingDashboard)
338+
if err == nil {
339+
reqLogger.Info("A dashboard instance already exists",
340+
"Namespace", existingDashboard.Namespace, "Name", existingDashboard.Name)
341+
342+
// See if we need to reconcile based on dashboard data only to allow users
343+
// to disable dashboard via label if so desired. Note that disabling it
344+
// will be reset if dashboard changes in newer version of operator.
345+
if existingDashboard.Data[entry.Name()] != dashboard.Data[entry.Name()] {
346+
reqLogger.Info("Dashboard data does not match expectation, reconciling",
347+
"Namespace", dashboard.Namespace, "Name", dashboard.Name)
348+
err := r.Client.Update(context.TODO(), dashboard)
349+
if err != nil {
350+
reqLogger.Error(err, "Error updating dashboard",
351+
"Namespace", dashboard.Namespace, "Name", dashboard.Name)
352+
}
353+
}
354+
continue
355+
}
356+
357+
if errors.IsNotFound(err) {
358+
reqLogger.Info("Creating new dashboard",
359+
"Namespace", dashboard.Namespace, "Name", dashboard.Name)
360+
err := r.Client.Create(context.TODO(), dashboard)
361+
if err != nil {
362+
reqLogger.Error(err, "Error creating a new dashboard",
363+
"Namespace", dashboard.Namespace, "Name", dashboard.Name)
364+
}
365+
}
366+
}
367+
}
368+
return nil
369+
}
370+
371+
func newDashboardConfigMap(filename string, namespace string) (*corev1.ConfigMap, error) {
372+
373+
name := strings.TrimSuffix(filename, filepath.Ext(filename))
374+
375+
objectMeta := metav1.ObjectMeta{
376+
Name: name,
377+
Namespace: namespace,
378+
Labels: map[string]string{
379+
"console.openshift.io/dashboard": "true",
380+
},
381+
}
382+
383+
content, err := dashboards.ReadFile(dashboardFolder + "/" + filename)
384+
if err != nil {
385+
return nil, err
386+
}
387+
388+
return &corev1.ConfigMap{
389+
ObjectMeta: objectMeta,
390+
Data: map[string]string{
391+
filename: string(content),
392+
},
393+
}, nil
394+
}
395+
295396
func newReadRole(namespace string) *rbacv1.Role {
296397
objectMeta := metav1.ObjectMeta{
297398
Name: fmt.Sprintf(readRoleNameFormat, namespace),

controllers/argocd_metrics_controller_test.go

+59
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ package controllers
1919
import (
2020
"context"
2121
"fmt"
22+
"path/filepath"
23+
"strings"
2224
"testing"
2325

2426
argoapp "github.com/argoproj-labs/argocd-operator/api/v1alpha1"
@@ -27,6 +29,7 @@ import (
2729
is "gotest.tools/assert/cmp"
2830
corev1 "k8s.io/api/core/v1"
2931
rbacv1 "k8s.io/api/rbac/v1"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3033
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3134
"k8s.io/apimachinery/pkg/runtime"
3235
"k8s.io/apimachinery/pkg/types"
@@ -287,3 +290,59 @@ func TestReconciler_add_prometheus_rule(t *testing.T) {
287290
assert.Equal(t, rule.Spec.Groups[0].Rules[0].Expr.StrVal, expr)
288291
}
289292
}
293+
294+
func TestReconciler_add_dashboard(t *testing.T) {
295+
296+
// Need to create openshift-config-managed namespace for dashboards
297+
ns := corev1.Namespace{
298+
ObjectMeta: v1.ObjectMeta{
299+
Name: dashboardNamespace,
300+
},
301+
}
302+
303+
// Need to create one configmap to test update existing versus create
304+
cm := corev1.ConfigMap{
305+
ObjectMeta: metav1.ObjectMeta{
306+
Name: "gitops-overview",
307+
Namespace: dashboardNamespace,
308+
},
309+
}
310+
311+
testCases := []struct {
312+
instanceName string
313+
namespace string
314+
}{
315+
{
316+
instanceName: argoCDInstanceName,
317+
namespace: "openshift-gitops",
318+
},
319+
}
320+
for _, tc := range testCases {
321+
r := newMetricsReconciler(t, tc.namespace, tc.instanceName)
322+
// Create dashboard namespace
323+
err := r.Client.Create(context.TODO(), &ns)
324+
assert.NilError(t, err)
325+
// Create update test dashboard
326+
err = r.Client.Create(context.TODO(), &cm)
327+
assert.NilError(t, err)
328+
329+
_, err = r.Reconcile(context.TODO(), newRequest(tc.namespace, tc.instanceName))
330+
assert.NilError(t, err)
331+
332+
entries, err := dashboards.ReadDir(dashboardFolder)
333+
assert.NilError(t, err)
334+
335+
for _, entry := range entries {
336+
name := strings.TrimSuffix(entry.Name(), filepath.Ext(entry.Name()))
337+
content, err := dashboards.ReadFile(dashboardFolder + "/" + entry.Name())
338+
assert.NilError(t, err)
339+
340+
dashboard := &corev1.ConfigMap{}
341+
err = r.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: dashboardNamespace}, dashboard)
342+
assert.NilError(t, err)
343+
344+
assert.Assert(t, dashboard.ObjectMeta.Labels["console.openshift.io/dashboard"] == "true")
345+
assert.Assert(t, dashboard.Data[entry.Name()] == string(content))
346+
}
347+
}
348+
}

0 commit comments

Comments
 (0)