Skip to content

Commit

Permalink
feat: expose grafana version in spec & status (#1473)
Browse files Browse the repository at this point in the history
  • Loading branch information
theSuess committed Apr 3, 2024
1 parent 720e742 commit ba4734b
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 6 deletions.
4 changes: 4 additions & 0 deletions api/v1beta1/grafana_types.go
Expand Up @@ -63,6 +63,8 @@ type GrafanaSpec struct {
Route *RouteOpenshiftV1 `json:"route,omitempty"`
// Service sets how the service object should look like with your grafana instance, contains a number of defaults.
Service *ServiceV1 `json:"service,omitempty"`
// Version specifies the version of Grafana to use for this deployment. It follows the same format as the docker.io/grafana/grafana tags
Version string `json:"version,omitempty"`
// Deployment sets how the deployment object should look like with your grafana instance, contains a number of defaults.
Deployment *DeploymentV1 `json:"deployment,omitempty"`
// PersistentVolumeClaim creates a PVC if you need to attach one to your grafana instance.
Expand Down Expand Up @@ -116,12 +118,14 @@ type GrafanaStatus struct {
Dashboards NamespacedResourceList `json:"dashboards,omitempty"`
Datasources NamespacedResourceList `json:"datasources,omitempty"`
Folders NamespacedResourceList `json:"folders,omitempty"`
Version string `json:"version,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status

// Grafana is the Schema for the grafanas API
// +kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description=""
// +kubebuilder:printcolumn:name="Stage",type="string",JSONPath=".status.stage",description=""
// +kubebuilder:printcolumn:name="Stage status",type="string",JSONPath=".status.stageStatus",description=""
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description=""
Expand Down
7 changes: 7 additions & 0 deletions config/crd/bases/grafana.integreatly.org_grafanas.yaml
Expand Up @@ -15,6 +15,9 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.version
name: Version
type: string
- jsonPath: .status.stage
name: Stage
type: string
Expand Down Expand Up @@ -3976,6 +3979,8 @@ spec:
x-kubernetes-map-type: atomic
type: array
type: object
version:
type: string
type: object
status:
properties:
Expand All @@ -3999,6 +4004,8 @@ spec:
type: string
stageStatus:
type: string
version:
type: string
type: object
type: object
served: true
Expand Down
10 changes: 10 additions & 0 deletions config/grafana.integreatly.org_grafanas.yaml
Expand Up @@ -15,6 +15,9 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.version
name: Version
type: string
- jsonPath: .status.stage
name: Stage
type: string
Expand Down Expand Up @@ -10037,6 +10040,11 @@ spec:
x-kubernetes-map-type: atomic
type: array
type: object
version:
description: Version specifies the version of Grafana to use for this
deployment. It follows the same format as the docker.io/grafana/grafana
tags
type: string
type: object
status:
description: GrafanaStatus defines the observed state of Grafana
Expand All @@ -10061,6 +10069,8 @@ spec:
type: string
stageStatus:
type: string
version:
type: string
type: object
type: object
served: true
Expand Down
17 changes: 17 additions & 0 deletions controllers/client/grafana_client.go
Expand Up @@ -124,6 +124,23 @@ func getAdminCredentials(ctx context.Context, c client.Client, grafana *v1beta1.
return credentials, nil
}

func NewHTTPClient(grafana *v1beta1.Grafana) *http.Client {
var timeout time.Duration
if grafana.Spec.Client != nil && grafana.Spec.Client.TimeoutSeconds != nil {
timeout = time.Duration(*grafana.Spec.Client.TimeoutSeconds)
if timeout < 0 {
timeout = 0
}
} else {
timeout = 10
}

return &http.Client{
Transport: NewInstrumentedRoundTripper(grafana.Name, metrics.GrafanaApiRequests, grafana.IsExternal()),
Timeout: time.Second * timeout,
}
}

func NewGeneratedGrafanaClient(ctx context.Context, c client.Client, grafana *v1beta1.Grafana) (*genapi.GrafanaHTTPAPI, error) {
var timeout time.Duration
if grafana.Spec.Client != nil && grafana.Spec.Client.TimeoutSeconds != nil {
Expand Down
47 changes: 47 additions & 0 deletions controllers/grafana_controller.go
Expand Up @@ -18,10 +18,13 @@ package controllers

import (
"context"
"encoding/json"
"fmt"
"reflect"
"time"

"github.com/go-logr/logr"
"github.com/grafana/grafana-operator/v5/controllers/config"
"github.com/grafana/grafana-operator/v5/controllers/metrics"
"github.com/grafana/grafana-operator/v5/controllers/reconcilers"
"github.com/grafana/grafana-operator/v5/controllers/reconcilers/grafana"
Expand All @@ -36,6 +39,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"

grafanav1beta1 "github.com/grafana/grafana-operator/v5/api/v1beta1"
client2 "github.com/grafana/grafana-operator/v5/controllers/client"
)

const (
Expand Down Expand Up @@ -86,9 +90,21 @@ func (r *GrafanaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
nextStatus.Stage = grafanav1beta1.OperatorStageComplete
nextStatus.StageStatus = grafanav1beta1.OperatorStageResultSuccess
nextStatus.AdminUrl = grafana.Spec.External.URL
v, err := r.getVersion(grafana)
if err != nil {
controllerLog.Error(err, "failed to get version from external instance")
}
nextStatus.Version = v
return r.updateStatus(grafana, nextStatus)
}

if grafana.Spec.Version == "" {
grafana.Spec.Version = config.GrafanaVersion
if err := r.Client.Update(ctx, grafana); err != nil {
return ctrl.Result{}, fmt.Errorf("updating grafana version in spec: %w", err)
}
}

for _, stage := range stages {
controllerLog.Info("running stage", "stage", stage)

Expand Down Expand Up @@ -120,12 +136,36 @@ func (r *GrafanaReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
}

if finished {
v, err := r.getVersion(grafana)
if err != nil {
controllerLog.Error(err, "failed to get version from instance")
}
nextStatus.Version = v
controllerLog.Info("grafana installation complete")
}

return r.updateStatus(grafana, nextStatus)
}

func (r *GrafanaReconciler) getVersion(cr *grafanav1beta1.Grafana) (string, error) {
cl := client2.NewHTTPClient(cr)

resp, err := cl.Get(cr.Status.AdminUrl + grafana.GrafanaHealthEndpoint)
if err != nil {
return "", fmt.Errorf("fetching version: %w", err)
}
data := struct {
Version string `json:"version"`
}{}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return "", fmt.Errorf("parsing health endpoint data: %w", err)
}
if data.Version == "" {
return "", fmt.Errorf("empty version received from server")
}
return data.Version, nil
}

func (r *GrafanaReconciler) updateStatus(cr *grafanav1beta1.Grafana, nextStatus *grafanav1beta1.GrafanaStatus) (ctrl.Result, error) {
if !reflect.DeepEqual(&cr.Status, nextStatus) {
nextStatus.DeepCopyInto(&cr.Status)
Expand All @@ -144,6 +184,13 @@ func (r *GrafanaReconciler) updateStatus(cr *grafanav1beta1.Grafana, nextStatus
RequeueAfter: RequeueDelay,
}, nil
}
if cr.Status.Version == "" {
r.Log.Info("version not yet found, requeuing")
return ctrl.Result{
Requeue: true,
RequeueAfter: RequeueDelay,
}, nil
}

return ctrl.Result{
Requeue: false,
Expand Down
7 changes: 5 additions & 2 deletions controllers/reconcilers/grafana/deployment_reconciler.go
Expand Up @@ -133,7 +133,10 @@ func getVolumeMounts(cr *v1beta1.Grafana, scheme *runtime.Scheme) []v1.VolumeMou
return mounts
}

func getGrafanaImage() string {
func getGrafanaImage(cr *v1beta1.Grafana) string {
if cr.Spec.Version != "" {
return fmt.Sprintf("%s:%s", config2.GrafanaImage, cr.Spec.Version)
}
grafanaImg := os.Getenv("RELATED_IMAGE_GRAFANA")
if grafanaImg == "" {
grafanaImg = fmt.Sprintf("%s:%s", config2.GrafanaImage, config2.GrafanaVersion)
Expand All @@ -144,7 +147,7 @@ func getGrafanaImage() string {
func getContainers(cr *v1beta1.Grafana, scheme *runtime.Scheme, vars *v1beta1.OperatorReconcileVars, openshiftPlatform bool) []v1.Container {
var containers []v1.Container

image := getGrafanaImage()
image := getGrafanaImage(cr)
plugins := model.GetPluginsConfigMap(cr, scheme)

// env var to restart containers if plugins change
Expand Down
29 changes: 27 additions & 2 deletions controllers/reconcilers/grafana/deployment_reconciler_test.go
Expand Up @@ -4,20 +4,45 @@ import (
"fmt"
"testing"

"github.com/grafana/grafana-operator/v5/api/v1beta1"
config2 "github.com/grafana/grafana-operator/v5/controllers/config"

"github.com/stretchr/testify/assert"
)

func Test_getGrafanaImage(t *testing.T) {
cr := &v1beta1.Grafana{
Spec: v1beta1.GrafanaSpec{
Version: "",
},
}

expectedDeploymentImage := fmt.Sprintf("%s:%s", config2.GrafanaImage, config2.GrafanaVersion)

assert.Equal(t, expectedDeploymentImage, getGrafanaImage())
assert.Equal(t, expectedDeploymentImage, getGrafanaImage(cr))
}

func Test_getGrafanaImage_specificVersion(t *testing.T) {
cr := &v1beta1.Grafana{
Spec: v1beta1.GrafanaSpec{
Version: "10.4.0",
},
}

expectedDeploymentImage := fmt.Sprintf("%s:10.4.0", config2.GrafanaImage)

assert.Equal(t, expectedDeploymentImage, getGrafanaImage(cr))
}

func Test_getGrafanaImage_withEnvironmentOverride(t *testing.T) {
cr := &v1beta1.Grafana{
Spec: v1beta1.GrafanaSpec{
Version: "",
},
}

expectedDeploymentImage := "I want this grafana image"
t.Setenv("RELATED_IMAGE_GRAFANA", expectedDeploymentImage)

assert.Equal(t, expectedDeploymentImage, getGrafanaImage())
assert.Equal(t, expectedDeploymentImage, getGrafanaImage(cr))
}
Expand Up @@ -15,6 +15,9 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.version
name: Version
type: string
- jsonPath: .status.stage
name: Stage
type: string
Expand Down Expand Up @@ -3976,6 +3979,8 @@ spec:
x-kubernetes-map-type: atomic
type: array
type: object
version:
type: string
type: object
status:
properties:
Expand All @@ -3999,6 +4004,8 @@ spec:
type: string
stageStatus:
type: string
version:
type: string
type: object
type: object
served: true
Expand Down
10 changes: 10 additions & 0 deletions deploy/kustomize/base/crds.yaml
Expand Up @@ -960,6 +960,9 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.version
name: Version
type: string
- jsonPath: .status.stage
name: Stage
type: string
Expand Down Expand Up @@ -10982,6 +10985,11 @@ spec:
x-kubernetes-map-type: atomic
type: array
type: object
version:
description: Version specifies the version of Grafana to use for this
deployment. It follows the same format as the docker.io/grafana/grafana
tags
type: string
type: object
status:
description: GrafanaStatus defines the observed state of Grafana
Expand All @@ -11006,6 +11014,8 @@ spec:
type: string
stageStatus:
type: string
version:
type: string
type: object
type: object
served: true
Expand Down
14 changes: 14 additions & 0 deletions docs/docs/api.md
Expand Up @@ -2235,6 +2235,13 @@ GrafanaSpec defines the desired state of Grafana
ServiceAccount sets how the ServiceAccount object should look like with your grafana instance, contains a number of defaults.<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>version</b></td>
<td>string</td>
<td>
Version specifies the version of Grafana to use for this deployment. It follows the same format as the docker.io/grafana/grafana tags<br/>
</td>
<td>false</td>
</tr></tbody>
</table>

Expand Down Expand Up @@ -17343,5 +17350,12 @@ GrafanaStatus defines the observed state of Grafana
<br/>
</td>
<td>false</td>
</tr><tr>
<td><b>version</b></td>
<td>string</td>
<td>
<br/>
</td>
<td>false</td>
</tr></tbody>
</table>
15 changes: 14 additions & 1 deletion tests/e2e/example-test/00-assert.yaml
Expand Up @@ -2,16 +2,29 @@ apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana
metadata:
name: grafana
spec:
version: 9.5.17
status:
(wildcard('http://grafana-service.*:3000', adminUrl || '')): true
stage: complete
stageStatus: success
version: 9.5.17
---
apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana
metadata:
name: external-grafana
status:
adminUrl: http://grafana-internal-service
adminUrl: (join('',['http://grafana-internal-service.',$namespace,':3000']))
stage: complete
stageStatus: success
version: 9.5.17
---
apiVersion: grafana.integreatly.org/v1beta1
kind: Grafana
metadata:
name: grafana-ten
status:
stage: complete
stageStatus: success
version: 10.3.5
2 changes: 1 addition & 1 deletion tests/e2e/example-test/00-create-external-grafana.yaml
Expand Up @@ -40,7 +40,7 @@ metadata:
dashboards: "external-grafana"
spec:
external:
url: http://grafana-internal-service
url: (join('',['http://grafana-internal-service.',$namespace,':3000']))
adminPassword:
name: grafana-internal-admin-credentials
key: GF_SECURITY_ADMIN_PASSWORD
Expand Down

0 comments on commit ba4734b

Please sign in to comment.