From 31e410cc96b4323c34a97025241ebefc4a2451ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominik=20S=C3=BC=C3=9F?= Date: Mon, 25 Mar 2024 15:38:40 +0100 Subject: [PATCH] feat: expose grafana version in spec & status --- api/v1beta1/grafana_types.go | 4 ++ .../grafana.integreatly.org_grafanas.yaml | 7 +++ config/grafana.integreatly.org_grafanas.yaml | 10 ++++ controllers/client/grafana_client.go | 17 +++++-- controllers/grafana_controller.go | 47 +++++++++++++++++++ .../grafana/deployment_reconciler.go | 7 ++- .../grafana.integreatly.org_grafanas.yaml | 7 +++ tests/e2e/example-test/00-assert.yaml | 15 +++++- .../00-create-external-grafana.yaml | 2 +- .../00-create-versioned-grafana.yaml | 18 +++++++ tests/e2e/example-test/chainsaw-test.yaml | 3 ++ 11 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 tests/e2e/example-test/00-create-versioned-grafana.yaml diff --git a/api/v1beta1/grafana_types.go b/api/v1beta1/grafana_types.go index 347c626d8..430192f2a 100644 --- a/api/v1beta1/grafana_types.go +++ b/api/v1beta1/grafana_types.go @@ -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. @@ -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="" diff --git a/config/crd/bases/grafana.integreatly.org_grafanas.yaml b/config/crd/bases/grafana.integreatly.org_grafanas.yaml index 231754236..975e9afcf 100644 --- a/config/crd/bases/grafana.integreatly.org_grafanas.yaml +++ b/config/crd/bases/grafana.integreatly.org_grafanas.yaml @@ -15,6 +15,9 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: + - jsonPath: .status.version + name: Version + type: string - jsonPath: .status.stage name: Stage type: string @@ -3976,6 +3979,8 @@ spec: x-kubernetes-map-type: atomic type: array type: object + version: + type: string type: object status: properties: @@ -3999,6 +4004,8 @@ spec: type: string stageStatus: type: string + version: + type: string type: object type: object served: true diff --git a/config/grafana.integreatly.org_grafanas.yaml b/config/grafana.integreatly.org_grafanas.yaml index b5c2a7d1b..bbdb39558 100644 --- a/config/grafana.integreatly.org_grafanas.yaml +++ b/config/grafana.integreatly.org_grafanas.yaml @@ -15,6 +15,9 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: + - jsonPath: .status.version + name: Version + type: string - jsonPath: .status.stage name: Stage type: string @@ -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 @@ -10061,6 +10069,8 @@ spec: type: string stageStatus: type: string + version: + type: string type: object type: object served: true diff --git a/controllers/client/grafana_client.go b/controllers/client/grafana_client.go index a69343a48..1d1c3e312 100644 --- a/controllers/client/grafana_client.go +++ b/controllers/client/grafana_client.go @@ -125,7 +125,7 @@ func getAdminCredentials(ctx context.Context, c client.Client, grafana *v1beta1. return credentials, nil } -func NewGrafanaClient(ctx context.Context, c client.Client, grafana *v1beta1.Grafana) (*grapi.Client, error) { +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) @@ -136,17 +136,24 @@ func NewGrafanaClient(ctx context.Context, c client.Client, grafana *v1beta1.Gra timeout = 10 } + return &http.Client{ + Transport: NewInstrumentedRoundTripper(grafana.Name, metrics.GrafanaApiRequests, grafana.IsExternal()), + Timeout: time.Second * timeout, + } +} + +func NewGrafanaClient(ctx context.Context, c client.Client, grafana *v1beta1.Grafana) (*grapi.Client, error) { + credentials, err := getAdminCredentials(ctx, c, grafana) if err != nil { return nil, err } + client := NewHTTPClient(grafana) + clientConfig := grapi.Config{ HTTPHeaders: nil, - Client: &http.Client{ - Transport: NewInstrumentedRoundTripper(grafana.Name, metrics.GrafanaApiRequests, grafana.IsExternal()), - Timeout: time.Second * timeout, - }, + Client: client, // TODO populate me OrgID: 0, // TODO populate me diff --git a/controllers/grafana_controller.go b/controllers/grafana_controller.go index 71c457450..950a771d8 100644 --- a/controllers/grafana_controller.go +++ b/controllers/grafana_controller.go @@ -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" @@ -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 ( @@ -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) @@ -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) @@ -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, diff --git a/controllers/reconcilers/grafana/deployment_reconciler.go b/controllers/reconcilers/grafana/deployment_reconciler.go index 671670170..a686c9945 100644 --- a/controllers/reconcilers/grafana/deployment_reconciler.go +++ b/controllers/reconcilers/grafana/deployment_reconciler.go @@ -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) @@ -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 diff --git a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanas.yaml b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanas.yaml index 231754236..975e9afcf 100644 --- a/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanas.yaml +++ b/deploy/helm/grafana-operator/crds/grafana.integreatly.org_grafanas.yaml @@ -15,6 +15,9 @@ spec: scope: Namespaced versions: - additionalPrinterColumns: + - jsonPath: .status.version + name: Version + type: string - jsonPath: .status.stage name: Stage type: string @@ -3976,6 +3979,8 @@ spec: x-kubernetes-map-type: atomic type: array type: object + version: + type: string type: object status: properties: @@ -3999,6 +4004,8 @@ spec: type: string stageStatus: type: string + version: + type: string type: object type: object served: true diff --git a/tests/e2e/example-test/00-assert.yaml b/tests/e2e/example-test/00-assert.yaml index 3064b0a3f..6600043a9 100644 --- a/tests/e2e/example-test/00-assert.yaml +++ b/tests/e2e/example-test/00-assert.yaml @@ -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 diff --git a/tests/e2e/example-test/00-create-external-grafana.yaml b/tests/e2e/example-test/00-create-external-grafana.yaml index bf4f9d01e..6315b931c 100644 --- a/tests/e2e/example-test/00-create-external-grafana.yaml +++ b/tests/e2e/example-test/00-create-external-grafana.yaml @@ -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 diff --git a/tests/e2e/example-test/00-create-versioned-grafana.yaml b/tests/e2e/example-test/00-create-versioned-grafana.yaml new file mode 100644 index 000000000..fcc5b0a65 --- /dev/null +++ b/tests/e2e/example-test/00-create-versioned-grafana.yaml @@ -0,0 +1,18 @@ +apiVersion: grafana.integreatly.org/v1beta1 +kind: Grafana +metadata: + name: grafana-ten + labels: + dashboards: "grafana" +spec: + client: + preferIngress: false + version: 10.3.5 + config: + log: + mode: "console" + auth: + disable_login_form: "false" + security: + admin_user: root + admin_password: secret diff --git a/tests/e2e/example-test/chainsaw-test.yaml b/tests/e2e/example-test/chainsaw-test.yaml index f15f819c7..df4789d30 100755 --- a/tests/e2e/example-test/chainsaw-test.yaml +++ b/tests/e2e/example-test/chainsaw-test.yaml @@ -4,6 +4,7 @@ kind: Test metadata: name: example-test spec: + template: true steps: - name: step-00 try: @@ -11,6 +12,8 @@ spec: file: 00-create-external-grafana.yaml - apply: file: 00-create-grafana.yaml + - apply: + file: 00-create-versioned-grafana.yaml - assert: file: 00-assert.yaml - name: step-01