forked from grafana/grafana-operator
-
Notifications
You must be signed in to change notification settings - Fork 0
/
grafana_reconciler.go
383 lines (332 loc) · 12.9 KB
/
grafana_reconciler.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package grafana
import (
"fmt"
"regexp"
"github.com/grafana-operator/grafana-operator/v4/api/integreatly/v1alpha1"
"github.com/grafana-operator/grafana-operator/v4/controllers/common"
"github.com/grafana-operator/grafana-operator/v4/controllers/config"
"github.com/grafana-operator/grafana-operator/v4/controllers/constants"
"github.com/grafana-operator/grafana-operator/v4/controllers/model"
v1 "k8s.io/api/core/v1"
v12 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type GrafanaReconciler struct {
DsHash string
ConfigHash string
CredentialsHash string
PluginsEnv string
Plugins *PluginsHelperImpl
}
func NewGrafanaReconciler() *GrafanaReconciler {
return &GrafanaReconciler{
DsHash: "",
ConfigHash: "",
PluginsEnv: "",
Plugins: NewPluginsHelper(),
}
}
func (i *GrafanaReconciler) Reconcile(state *common.ClusterState, cr *v1alpha1.Grafana) common.DesiredClusterState {
desired := common.DesiredClusterState{}
desired = desired.AddAction(i.getGrafanaAdminUserSecretDesiredState(state, cr))
desired = desired.AddAction(i.getGrafanaServiceDesiredState(state, cr))
if cr.UsedPersistentVolume() {
desired = desired.AddAction(i.getGrafanaDataPvcDesiredState(state, cr))
}
desired = desired.AddAction(i.getGrafanaServiceAccountDesiredState(state, cr))
desired = desired.AddActions(i.getGrafanaConfigDesiredState(state, cr))
desired = desired.AddAction(i.getGrafanaDatasourceConfigDesiredState(state, cr))
desired = desired.AddAction(i.getGrafanaExternalAccessDesiredState(state, cr))
// Consolidate plugins
// No action, will update init container env var
desired = desired.AddAction(i.getGrafanaPluginsDesiredState(cr))
// Reconcile the deployment last because it depends on the configuration
// and plugins list computed in previous steps
desired = desired.AddAction(i.getGrafanaDeploymentDesiredState(state, cr))
desired = desired.AddActions(i.getEnvVarsDesiredState(state, cr))
// Check Deployment and Route readiness
desired = desired.AddActions(i.getGrafanaReadiness(state, cr))
return desired
}
func (i *GrafanaReconciler) getGrafanaReadiness(state *common.ClusterState, cr *v1alpha1.Grafana) []common.ClusterAction {
var actions []common.ClusterAction
cfg := config.GetControllerConfig()
openshift := cfg.GetConfigBool(config.ConfigOpenshift, false)
if openshift && cr.Spec.Ingress != nil && cr.Spec.Ingress.Enabled && !cr.GetPreferServiceValue() {
// On OpenShift, check the route, only if preferService is false
actions = append(actions, common.RouteReadyAction{
Ref: state.GrafanaRoute,
Msg: "check route readiness",
})
}
if !openshift && cr.Spec.Ingress != nil && cr.Spec.Ingress.Enabled && !cr.GetPreferServiceValue() {
// On vanilla Kubernetes, check the ingress,only if preferService is false
actions = append(actions, common.IngressReadyAction{
Ref: state.GrafanaIngress,
Msg: "check ingress readiness",
})
}
return append(actions, common.DeploymentReadyAction{
Ref: state.GrafanaDeployment,
Msg: "check deployment readiness",
})
}
func (i *GrafanaReconciler) getGrafanaServiceDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if state.GrafanaService == nil {
return common.GenericCreateAction{
Ref: model.GrafanaService(cr),
Msg: "create grafana service",
}
}
if cr.Status.PreviousServiceName != "" && state.GrafanaService.Name != "" {
// if the previously known service is not the current service then delete the previous service
// validate the service
if cr.Status.PreviousServiceName != state.GrafanaService.Name && i.validateServiceName(cr.Spec.Service.Name) {
serviceName := cr.Status.PreviousServiceName
// reset the status before next loop
cr.Status.PreviousServiceName = ""
return common.GenericDeleteAction{
Ref: &v1.Service{
ObjectMeta: v12.ObjectMeta{
Name: serviceName,
Namespace: cr.Namespace,
},
},
Msg: "delete obsolete grafana service",
}
}
}
if cr.Status.PreviousServiceName == "" {
cr.Status.PreviousServiceName = state.GrafanaService.Name
}
return common.GenericUpdateAction{
Ref: model.GrafanaServiceReconciled(cr, state.GrafanaService),
Msg: "update grafana service",
}
}
func (i *GrafanaReconciler) getGrafanaDataPvcDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if state.GrafanaDataPersistentVolumeClaim == nil {
return common.GenericCreateAction{
Ref: model.GrafanaDataPVC(cr),
Msg: "create grafana data persistentVolumeClaim",
}
}
return common.GenericUpdateAction{
Ref: model.GrafanaPVCReconciled(cr, state.GrafanaDataPersistentVolumeClaim),
Msg: "update grafana data persistentVolumeClaim",
}
}
func (i *GrafanaReconciler) getGrafanaServiceAccountDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if cr.Spec.ServiceAccount != nil && cr.Spec.ServiceAccount.Skip != nil && *cr.Spec.ServiceAccount.Skip {
return nil
}
if state.GrafanaServiceAccount == nil {
return common.GenericCreateAction{
Ref: model.GrafanaServiceAccount(cr),
Msg: "create grafana service account",
}
}
return common.GenericUpdateAction{
Ref: model.GrafanaServiceAccountReconciled(cr, state.GrafanaServiceAccount),
Msg: "update grafana service account",
}
}
func (i *GrafanaReconciler) getGrafanaConfigDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) []common.ClusterAction {
actions := []common.ClusterAction{}
if state.GrafanaConfig == nil {
config := model.GrafanaConfig(cr)
// Store the last config hash for the duration of this reconciliation for
// later usage in the deployment
i.ConfigHash = config.Annotations[constants.LastConfigAnnotation]
actions = append(actions, common.GenericCreateAction{
Ref: config,
Msg: "create grafana config",
})
} else {
config := model.GrafanaConfigReconciled(cr, state.GrafanaConfig)
i.ConfigHash = config.Annotations[constants.LastConfigAnnotation]
actions = append(actions, common.GenericUpdateAction{
Ref: config,
Msg: "update grafana config",
})
}
return actions
}
func (i *GrafanaReconciler) getGrafanaDatasourceConfigDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
// Only create the datasources configmap if it doesn't exist. Updates
// are handled by the datasources controller
if state.GrafanaDataSourceConfig == nil {
return common.GenericCreateAction{
Ref: model.GrafanaDatasourcesConfig(cr),
Msg: "create grafanadatasource config",
}
} else {
if state.GrafanaDataSourceConfig.Annotations != nil {
i.DsHash = state.GrafanaDataSourceConfig.Annotations[constants.LastConfigAnnotation]
}
}
return nil
}
func (i *GrafanaReconciler) getGrafanaExternalAccessDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
cfg := config.GetControllerConfig()
isOpenshift := cfg.GetConfigBool(config.ConfigOpenshift, false)
if cr.Spec.Ingress == nil || !cr.Spec.Ingress.Enabled {
// external access not enabled: remote the route/ingress if it exists or
// do nothing
if isOpenshift && state.GrafanaRoute != nil {
return common.GenericDeleteAction{
Ref: state.GrafanaRoute,
Msg: "delete grafana route",
}
} else if !isOpenshift && state.GrafanaIngress != nil {
return common.GenericDeleteAction{
Ref: state.GrafanaIngress,
Msg: "delete grafana ingress",
}
}
return nil
} else {
// external access enabled: create route/ingress
if isOpenshift {
return i.getGrafanaRouteDesiredState(state, cr)
}
return i.getGrafanaIngressDesiredState(state, cr)
}
}
func (i *GrafanaReconciler) getGrafanaAdminUserSecretDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if cr.Spec.Deployment != nil && cr.Spec.Deployment.SkipCreateAdminAccount != nil && *cr.Spec.Deployment.SkipCreateAdminAccount {
return nil
}
if state.AdminSecret == nil {
secret := model.AdminSecret(cr)
i.CredentialsHash = secret.Annotations[constants.LastCredentialsAnnotation]
return common.GenericCreateAction{
Ref: secret,
Msg: "create admin credentials secret",
}
}
secret := model.AdminSecretReconciled(cr, state.AdminSecret)
i.CredentialsHash = secret.Annotations[constants.LastCredentialsAnnotation]
return common.GenericUpdateAction{
Ref: secret,
Msg: "update admin credentials secret",
}
}
func (i *GrafanaReconciler) getGrafanaIngressDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if state.GrafanaIngress == nil {
return common.GenericCreateAction{
Ref: model.GrafanaIngress(cr),
Msg: "create grafana ingress",
}
}
return common.GenericUpdateAction{
Ref: model.GrafanaIngressReconciled(cr, state.GrafanaIngress),
Msg: "update grafana ingress",
}
}
func (i *GrafanaReconciler) getGrafanaRouteDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if state.GrafanaRoute == nil {
return common.GenericCreateAction{
Ref: model.GrafanaRoute(cr),
Msg: "create grafana route",
}
}
return common.GenericUpdateAction{
Ref: model.GrafanaRouteReconciled(cr, state.GrafanaRoute),
Msg: "update grafana route",
}
}
func (i *GrafanaReconciler) getGrafanaDeploymentDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) common.ClusterAction {
if state.GrafanaDeployment == nil {
return common.GenericCreateAction{
Ref: model.GrafanaDeployment(cr, i.ConfigHash, i.DsHash, i.CredentialsHash),
Msg: "create grafana deployment",
}
}
return common.GenericUpdateAction{
Ref: model.GrafanaDeploymentReconciled(cr, state.GrafanaDeployment,
i.ConfigHash, i.PluginsEnv, i.DsHash, i.CredentialsHash),
Msg: "update grafana deployment",
}
}
func (i *GrafanaReconciler) getEnvVarsDesiredState(state *common.ClusterState, cr *v1alpha1.Grafana) []common.ClusterAction {
if state.GrafanaDeployment == nil {
return nil
}
// Don't look for external admin credentials if the operator created an account
if cr.Spec.Deployment != nil && cr.Spec.Deployment.SkipCreateAdminAccount != nil && !*cr.Spec.Deployment.SkipCreateAdminAccount {
return nil
}
var actions []common.ClusterAction
for _, container := range state.GrafanaDeployment.Spec.Template.Spec.Containers {
if container.Name != "grafana" {
continue
}
for _, source := range container.EnvFrom {
if source.ConfigMapRef != nil && source.ConfigMapRef.Name != "" {
actions = append(actions, common.ExposeConfigMapEnvVarAction{
Ref: source.ConfigMapRef,
Namespace: cr.Namespace,
Msg: fmt.Sprintf("looking for admin credentials in config map %s", source.ConfigMapRef.Name),
})
} else if source.SecretRef != nil && source.SecretRef.Name != "" {
actions = append(actions, common.ExposeSecretEnvVarAction{
Ref: source.SecretRef,
Namespace: cr.Namespace,
Msg: fmt.Sprintf("looking for admin credentials in secret %s", source.SecretRef.Name),
})
}
}
}
return actions
}
func (i *GrafanaReconciler) getGrafanaPluginsDesiredState(cr *v1alpha1.Grafana) common.ClusterAction {
// Fetch all plugins of all dashboards
requestedPlugins := config.GetControllerConfig().GetAllPlugins()
// Consolidate plugins and create the new list of plugin requirements
// If 'updated' is false then no changes have to be applied
filteredPlugins, updated := i.Plugins.FilterPlugins(cr, requestedPlugins)
if updated {
i.reconcilePlugins(cr, filteredPlugins)
// Build the new list of plugins for the init container to consume
i.PluginsEnv = i.Plugins.BuildEnv(cr)
// Reset the list of known dashboards to force the dashboard controller
// to reimport them
cfg := config.GetControllerConfig()
cfg.InvalidateDashboards()
return common.LogAction{
Msg: fmt.Sprintf("plugins updated to %s", i.PluginsEnv),
}
} else {
// Rebuild the env var from the installed plugins
i.PluginsEnv = i.Plugins.BuildEnv(cr)
return common.LogAction{
Msg: "plugins unchanged",
}
}
}
func (i *GrafanaReconciler) reconcilePlugins(cr *v1alpha1.Grafana, plugins v1alpha1.PluginList) {
var validPlugins []v1alpha1.GrafanaPlugin // nolint
var failedPlugins []v1alpha1.GrafanaPlugin // nolint
for _, plugin := range plugins {
if !i.Plugins.PluginExists(plugin) {
log.V(1).Info(fmt.Sprintf("invalid plugin: %s@%s", plugin.Name, plugin.Version))
failedPlugins = append(failedPlugins, plugin)
continue
}
log.V(1).Info(fmt.Sprintf("installing plugin: %s@%s", plugin.Name, plugin.Version))
validPlugins = append(validPlugins, plugin)
}
cr.Status.InstalledPlugins = validPlugins
cr.Status.FailedPlugins = failedPlugins
}
func (i *GrafanaReconciler) validateServiceName(string string) bool {
// a DNS-1035 label must consist of lower case alphanumeric
// characters or '-', start with an alphabetic character, and end with an
// alphanumeric character (e.g. 'my-name', or 'abc-123', regex used for
// validation is '[a-z]([-a-z0-9]*[a-z0-9])?
b, err := regexp.MatchString("[a-z]([-a-z0-9]*[a-z0-9])?", string)
if err != nil {
return false
}
return b
}