From 7eacd4a96a52fbcfc62a950bb33251160b21d3ec Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Mon, 28 Jan 2019 09:21:25 +0100 Subject: [PATCH 1/5] add plugins volume --- templates/grafana-deployment.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/templates/grafana-deployment.yaml b/templates/grafana-deployment.yaml index 0d8575752..742405249 100644 --- a/templates/grafana-deployment.yaml +++ b/templates/grafana-deployment.yaml @@ -49,6 +49,8 @@ spec: name: grafana-logs - mountPath: /etc/grafana/ name: {{ .GrafanaConfigMapName }} + - mountPath: /var/lib/grafana/plugins + name: grafana-plugins dnsPolicy: ClusterFirst restartPolicy: Always schedulerName: default-scheduler @@ -73,3 +75,5 @@ spec: name: grafana-data - emptyDir: {} name: grafana-logs + - emptyDir: {} + name: grafana-plugins From 817ca109061402f357b7f6890344b5a05a6df240 Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Sun, 10 Mar 2019 23:38:27 +0100 Subject: [PATCH 2/5] implement plugin handling --- .../integreatly/v1alpha1/grafana_types.go | 8 ++- .../v1alpha1/grafanadashboard_types.go | 5 +- .../v1alpha1/zz_generated.deepcopy.go | 30 ++++++++- pkg/controller/grafana/grafana_controller.go | 53 +++++++++++++--- pkg/controller/grafana/kubeHelper.go | 38 ++++++++++-- pkg/controller/grafana/pluginsHelper.go | 62 +++++++++++++++++++ templates/grafana-deployment.yaml | 3 + 7 files changed, 182 insertions(+), 17 deletions(-) create mode 100644 pkg/controller/grafana/pluginsHelper.go diff --git a/pkg/apis/integreatly/v1alpha1/grafana_types.go b/pkg/apis/integreatly/v1alpha1/grafana_types.go index 09c2b6255..6207a9f23 100644 --- a/pkg/apis/integreatly/v1alpha1/grafana_types.go +++ b/pkg/apis/integreatly/v1alpha1/grafana_types.go @@ -19,7 +19,8 @@ type GrafanaSpec struct { type GrafanaStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Phase int `json:"phase"` + Phase int `json:"phase"` + InstalledPlugins []GrafanaPlugin `json:"installedPlugins,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -43,6 +44,11 @@ type GrafanaList struct { Items []Grafana `json:"items"` } +type GrafanaPlugin struct { + Name string + Version string +} + func init() { SchemeBuilder.Register(&Grafana{}, &GrafanaList{}) } diff --git a/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go b/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go index 86c71f6e8..8771ef7e9 100644 --- a/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go +++ b/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go @@ -11,8 +11,9 @@ import ( type GrafanaDashboardSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Json string `json:"json"` - Name string `json:"name"` + Json string `json:"json"` + Name string `json:"name"` + Plugins []GrafanaPlugin `json:"plugins,omitempty"` } // GrafanaDashboardStatus defines the observed state of GrafanaDashboard diff --git a/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go index 51cd3f3ce..555f8c1bc 100644 --- a/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go @@ -31,7 +31,7 @@ func (in *Grafana) DeepCopyInto(out *Grafana) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -58,7 +58,7 @@ func (in *GrafanaDashboard) DeepCopyInto(out *GrafanaDashboard) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) out.Status = in.Status return } @@ -117,6 +117,11 @@ func (in *GrafanaDashboardList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaDashboardSpec) DeepCopyInto(out *GrafanaDashboardSpec) { *out = *in + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]GrafanaPlugin, len(*in)) + copy(*out, *in) + } return } @@ -179,6 +184,22 @@ func (in *GrafanaList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaPlugin) DeepCopyInto(out *GrafanaPlugin) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaPlugin. +func (in *GrafanaPlugin) DeepCopy() *GrafanaPlugin { + if in == nil { + return nil + } + out := new(GrafanaPlugin) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaSpec) DeepCopyInto(out *GrafanaSpec) { *out = *in @@ -203,6 +224,11 @@ func (in *GrafanaSpec) DeepCopy() *GrafanaSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaStatus) DeepCopyInto(out *GrafanaStatus) { *out = *in + if in.InstalledPlugins != nil { + in, out := &in.InstalledPlugins, &out.InstalledPlugins + *out = make([]GrafanaPlugin, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/controller/grafana/grafana_controller.go b/pkg/controller/grafana/grafana_controller.go index 06cb04302..7fd853dd1 100644 --- a/pkg/controller/grafana/grafana_controller.go +++ b/pkg/controller/grafana/grafana_controller.go @@ -46,6 +46,7 @@ func newReconciler(mgr manager.Manager) reconcile.Reconciler { client: mgr.GetClient(), scheme: mgr.GetScheme(), helper: newKubeHelper(), + plugins: newPluginsHelper(), } } @@ -85,6 +86,7 @@ type ReconcileGrafana struct { client client.Client scheme *runtime.Scheme helper *KubeHelperImpl + plugins *PluginsHelperImpl } // Reconcile reads that state of the cluster for a Grafana object and makes changes based on the state read @@ -143,7 +145,7 @@ func (r *ReconcileGrafana) ReconcileNamespaces(cr *integreatly.Grafana) (reconci } else { if len(dashboards.Items) >= 1 { for _, d := range dashboards.Items { - r.ReconcileDashboards(cr.Namespace, d) + r.ReconcileDashboards(cr, d) } } } @@ -155,17 +157,52 @@ func (r *ReconcileGrafana) ReconcileNamespaces(cr *integreatly.Grafana) (reconci return reconcile.Result{RequeueAfter: time.Second * 10}, nil } -func (r *ReconcileGrafana) ReconcileDashboards(monitoringNamespace string, d integreatly.GrafanaDashboard) { - if d.Status.Created { - log.Info(fmt.Sprintf("Dashboard %s already created", d.Name)) - return - } - +func (r *ReconcileGrafana) ReconcileDashboards(cr *integreatly.Grafana, d integreatly.GrafanaDashboard) { log.Info(fmt.Sprintf("Reconciling dashboard: %s", d.Name)) - err := r.helper.updateDashboard(monitoringNamespace, d.Namespace, &d) + err := r.helper.updateDashboard(cr.Namespace, d.Namespace, &d) if err != nil { log.Error(err, "Error updating dashboard config") } + + err = r.ReconcilePlugins(cr, d) + if err != nil { + log.Error(err, "Error reconciling grafana plugins") + } +} + +func (r *ReconcileGrafana) ReconcilePlugins(cr *integreatly.Grafana, d integreatly.GrafanaDashboard) error { + if len(d.Spec.Plugins) > 0 { + pluginsAdded := false + for _, plugin := range d.Spec.Plugins { + log.Info(fmt.Sprintf("Processing requested plugin %s", plugin.Name)) + if r.plugins.pluginInstalled(plugin, cr) { + log.Info(fmt.Sprintf("%s is already installed", plugin.Name)) + continue + } + + if r.plugins.pluginExists(plugin) == false { + log.Info(fmt.Sprintf("Unknown plugin %s", plugin.Name)) + continue + } + + log.Info("Adding new plugin %s", plugin.Name) + cr.Status.InstalledPlugins = append(cr.Status.InstalledPlugins, plugin) + pluginsAdded = true + } + + if pluginsAdded { + err := r.client.Update(context.TODO(), cr) + if err != nil { + return err + } + + newEnv := r.plugins.buildEnv(cr) + err = r.helper.updateGrafanaDeployment(cr.Namespace, newEnv) + return err + } + } + + return nil } func (r *ReconcileGrafana) CreateConfigFiles(cr *integreatly.Grafana) (reconcile.Result, error) { diff --git a/pkg/controller/grafana/kubeHelper.go b/pkg/controller/grafana/kubeHelper.go index d81aeacaf..fec97d87e 100644 --- a/pkg/controller/grafana/kubeHelper.go +++ b/pkg/controller/grafana/kubeHelper.go @@ -3,16 +3,13 @@ package grafana import ( "github.com/integr8ly/grafana-operator/pkg/apis/integreatly/v1alpha1" gr "github.com/integr8ly/grafana-operator/pkg/client/versioned" + apps "k8s.io/api/apps/v1" "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "sigs.k8s.io/controller-runtime/pkg/client/config" ) -type KubeHelper interface { - listNamespaces() -} - type KubeHelperImpl struct { k8client *kubernetes.Clientset grclient *gr.Clientset @@ -86,3 +83,36 @@ func (h KubeHelperImpl) updateDashboard(monitoringNamespace string, dashboardNam return err } + +func (h KubeHelperImpl) getGrafanaDeployment(namespaceName string) (*apps.Deployment, error) { + opts := metav1.GetOptions{} + return h.k8client.AppsV1().Deployments(namespaceName).Get(GrafanaDeploymentName, opts) +} + + +func (h KubeHelperImpl) updateGrafanaDeployment(monitoringNamespace string, newEnv string) error { + deployment, err := h.getGrafanaDeployment(monitoringNamespace) + if err != nil { + return err + } + + updated := false + for _, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == "grafana" { + for _, env := range container.Env { + if env.Name == PluginsEnvVar { + env.Value = newEnv + updated = true + break + } + } + } + } + + if updated { + _, err := h.k8client.AppsV1().Deployments(monitoringNamespace).Update(deployment) + return err + } + + return nil +} diff --git a/pkg/controller/grafana/pluginsHelper.go b/pkg/controller/grafana/pluginsHelper.go new file mode 100644 index 000000000..d8afcf2b1 --- /dev/null +++ b/pkg/controller/grafana/pluginsHelper.go @@ -0,0 +1,62 @@ +package grafana + +import ( + "fmt" + integreatly "github.com/integr8ly/grafana-operator/pkg/apis/integreatly/v1alpha1" + "net/http" + "strings" +) + +const ( + PluginsEnvVar = "GRAFANA_PLUGINS" + PluginsUrl = "https://grafana.com/api/plugins/%s/versions/%s" +) + +type PluginsHelperImpl struct { + BaseUrl string +} + +func newPluginsHelper() *PluginsHelperImpl { + helper := new(PluginsHelperImpl) + helper.BaseUrl = PluginsUrl + return helper +} + +// Query the Grafana plugin database for the given plugin and version +// A 200 OK response indicates that the plugin exists and can be downloaded +func (h *PluginsHelperImpl) pluginExists(plugin integreatly.GrafanaPlugin) bool { + url := fmt.Sprintf(h.BaseUrl, plugin.Name, plugin.Version) + resp, err := http.Get(url) + if err != nil { + return false + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return false + } + + return true +} + +// Turns an array of plugins into a string representation of the form +// `:,...` that is used as the value for the GRAFANA_PLUGINS +// environment variable +func (h *PluginsHelperImpl) buildEnv(cr *integreatly.Grafana) string { + var env []string + for _, plugin := range cr.Status.InstalledPlugins { + env = append(env, fmt.Sprintf("%s:%s", plugin.Name, plugin.Version)) + } + return strings.Join(env, ",") +} + +// Checks if a given plugin is already installed. We do not allow to install +// multiple versions of the same plugin +func (h *PluginsHelperImpl) pluginInstalled(plugin integreatly.GrafanaPlugin, cr *integreatly.Grafana) bool { + for _, installedPlugin := range cr.Status.InstalledPlugins { + if installedPlugin.Name == plugin.Name { + return true + } + } + return false +} diff --git a/templates/grafana-deployment.yaml b/templates/grafana-deployment.yaml index 742405249..49941b657 100644 --- a/templates/grafana-deployment.yaml +++ b/templates/grafana-deployment.yaml @@ -25,6 +25,9 @@ spec: spec: containers: - image: '{{ .GrafanaImage }}:{{ .GrafanaVersion }}' + env: + - name: GRAFANA_PLUGINS + - value: "" args: - -config=/etc/grafana/grafana.ini imagePullPolicy: IfNotPresent From 6eeb88c46200ad807eaac59f59164a7b62f573bf Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Mon, 11 Mar 2019 10:16:23 +0100 Subject: [PATCH 3/5] add init container --- Gopkg.lock | 36 ++++++ Makefile | 6 +- deploy/crds/GrafanaDashboard.yaml | 21 ++++ deploy/examples/GrafanaDashboard.yaml | 11 +- .../integreatly/v1alpha1/grafana_types.go | 16 +-- .../v1alpha1/grafanadashboard_types.go | 15 ++- pkg/apis/integreatly/v1alpha1/pluginsList.go | 41 +++++++ .../v1alpha1/zz_generated.deepcopy.go | 78 ++++++++---- .../v1alpha1/zz_generated.defaults.go | 16 +++ pkg/controller/grafana/grafana_controller.go | 112 +++++++----------- pkg/controller/grafana/kubeHelper.go | 28 +++-- pkg/controller/grafana/pluginsHelper.go | 72 +++++++++-- templates/grafana-deployment.yaml | 16 ++- 13 files changed, 337 insertions(+), 131 deletions(-) create mode 100644 pkg/apis/integreatly/v1alpha1/pluginsList.go create mode 100644 pkg/apis/integreatly/v1alpha1/zz_generated.defaults.go diff --git a/Gopkg.lock b/Gopkg.lock index 5075bfa5a..e2cafbb8b 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -556,6 +556,7 @@ name = "k8s.io/client-go" packages = [ "discovery", + "discovery/fake", "dynamic", "kubernetes", "kubernetes/scheme", @@ -597,6 +598,7 @@ "rest", "rest/watch", "restmapper", + "testing", "third_party/forked/golang/template", "tools/auth", "tools/cache", @@ -619,6 +621,7 @@ "util/integer", "util/jsonpath", "util/retry", + "util/workqueue", ] pruneopts = "NT" revision = "1f13a808da65775f22cbf47862c4e5898d8f4ca1" @@ -704,14 +707,24 @@ "pkg/client", "pkg/client/apiutil", "pkg/client/config", + "pkg/controller", + "pkg/controller/controllerutil", + "pkg/event", + "pkg/handler", + "pkg/internal/controller", "pkg/internal/recorder", "pkg/leaderelection", "pkg/manager", "pkg/patch", + "pkg/predicate", + "pkg/reconcile", "pkg/recorder", "pkg/runtime/inject", "pkg/runtime/log", + "pkg/runtime/scheme", "pkg/runtime/signals", + "pkg/source", + "pkg/source/internal", "pkg/webhook/admission", "pkg/webhook/admission/types", "pkg/webhook/types", @@ -724,12 +737,28 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/ghodss/yaml", "github.com/operator-framework/operator-sdk/pkg/k8sutil", "github.com/operator-framework/operator-sdk/pkg/leader", "github.com/operator-framework/operator-sdk/pkg/ready", "github.com/operator-framework/operator-sdk/version", + "k8s.io/api/apps/v1", + "k8s.io/api/core/v1", + "k8s.io/apimachinery/pkg/api/errors", + "k8s.io/apimachinery/pkg/apis/meta/v1", + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured", "k8s.io/apimachinery/pkg/runtime", + "k8s.io/apimachinery/pkg/runtime/schema", + "k8s.io/apimachinery/pkg/runtime/serializer", + "k8s.io/apimachinery/pkg/types", + "k8s.io/apimachinery/pkg/watch", + "k8s.io/client-go/discovery", + "k8s.io/client-go/discovery/fake", + "k8s.io/client-go/kubernetes", "k8s.io/client-go/plugin/pkg/client/auth/gcp", + "k8s.io/client-go/rest", + "k8s.io/client-go/testing", + "k8s.io/client-go/util/flowcontrol", "k8s.io/code-generator/cmd/client-gen", "k8s.io/code-generator/cmd/conversion-gen", "k8s.io/code-generator/cmd/deepcopy-gen", @@ -738,10 +767,17 @@ "k8s.io/code-generator/cmd/lister-gen", "k8s.io/code-generator/cmd/openapi-gen", "k8s.io/gengo/args", + "sigs.k8s.io/controller-runtime/pkg/client", "sigs.k8s.io/controller-runtime/pkg/client/config", + "sigs.k8s.io/controller-runtime/pkg/controller", + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil", + "sigs.k8s.io/controller-runtime/pkg/handler", "sigs.k8s.io/controller-runtime/pkg/manager", + "sigs.k8s.io/controller-runtime/pkg/reconcile", "sigs.k8s.io/controller-runtime/pkg/runtime/log", + "sigs.k8s.io/controller-runtime/pkg/runtime/scheme", "sigs.k8s.io/controller-runtime/pkg/runtime/signals", + "sigs.k8s.io/controller-runtime/pkg/source", ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index a09ce1ac9..0c7e683d6 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,9 @@ -ORG=integreatly +ORG=pb82 NAMESPACE=application-monitoring PROJECT=grafana-operator -REG=quay.io +REG=docker.io SHELL=/bin/bash -TAG=0.0.1 +TAG=latest PKG=github.com/integr8ly/grafana-operator TEST_DIRS?=$(shell sh -c "find $(TOP_SRC_DIRS) -name \\*_test.go -exec dirname {} \\; | sort | uniq") TEST_POD_NAME=grafana-operator-test diff --git a/deploy/crds/GrafanaDashboard.yaml b/deploy/crds/GrafanaDashboard.yaml index 622ef740d..81a19aa8b 100644 --- a/deploy/crds/GrafanaDashboard.yaml +++ b/deploy/crds/GrafanaDashboard.yaml @@ -11,3 +11,24 @@ spec: singular: grafanadashboard scope: Namespaced version: v1alpha1 + validation: + openAPIV3Schema: + properties: + status: + properties: + messages: + type: array + items: + description: Dashboard Status Message + type: object + spec: + properties: + name: + type: string + json: + type: string + plugins: + type: array + items: + description: Grafana Plugin Object + type: object \ No newline at end of file diff --git a/deploy/examples/GrafanaDashboard.yaml b/deploy/examples/GrafanaDashboard.yaml index ad0da85b1..14e67bb56 100644 --- a/deploy/examples/GrafanaDashboard.yaml +++ b/deploy/examples/GrafanaDashboard.yaml @@ -1,7 +1,12 @@ apiVersion: integreatly.org/v1alpha1 kind: GrafanaDashboard metadata: - name: example-grafanadashboard + name: example spec: - # Add fields here - size: 3 + name: dashboard.json + json: "{}" + plugins: + - name: "grafana-piechart-panel" + version: "1.3.6" + - name: "grafana-clock-panel" + version: "1.0.2" diff --git a/pkg/apis/integreatly/v1alpha1/grafana_types.go b/pkg/apis/integreatly/v1alpha1/grafana_types.go index 6207a9f23..5a6907c83 100644 --- a/pkg/apis/integreatly/v1alpha1/grafana_types.go +++ b/pkg/apis/integreatly/v1alpha1/grafana_types.go @@ -19,8 +19,15 @@ type GrafanaSpec struct { type GrafanaStatus struct { // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Phase int `json:"phase"` - InstalledPlugins []GrafanaPlugin `json:"installedPlugins,omitempty"` + Phase int `json:"phase"` + InstalledPlugins PluginList `json:"installedPlugins"` +} + +// GrafanaPlugin contains information about a single plugin +type GrafanaPlugin struct { + Name string `json:"name"` + Version string `json:"version"` + Origin *GrafanaDashboard `json:"-"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -44,11 +51,6 @@ type GrafanaList struct { Items []Grafana `json:"items"` } -type GrafanaPlugin struct { - Name string - Version string -} - func init() { SchemeBuilder.Register(&Grafana{}, &GrafanaList{}) } diff --git a/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go b/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go index 8771ef7e9..f9022ab5c 100644 --- a/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go +++ b/pkg/apis/integreatly/v1alpha1/grafanadashboard_types.go @@ -11,16 +11,14 @@ import ( type GrafanaDashboardSpec struct { // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Json string `json:"json"` - Name string `json:"name"` - Plugins []GrafanaPlugin `json:"plugins,omitempty"` + Json string `json:"json"` + Name string `json:"name"` + Plugins PluginList `json:"plugins"` } // GrafanaDashboardStatus defines the observed state of GrafanaDashboard type GrafanaDashboardStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file - Created bool `json: "created"` + Messages []GrafanaDashboardStatusMessage `json:"messages"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -44,6 +42,11 @@ type GrafanaDashboardList struct { Items []GrafanaDashboard `json:"items"` } +type GrafanaDashboardStatusMessage struct { + Message string `json:"message"` + Timestamp string `json:"timestamp"` +} + func init() { SchemeBuilder.Register(&GrafanaDashboard{}, &GrafanaDashboardList{}) } diff --git a/pkg/apis/integreatly/v1alpha1/pluginsList.go b/pkg/apis/integreatly/v1alpha1/pluginsList.go new file mode 100644 index 000000000..94cfecea3 --- /dev/null +++ b/pkg/apis/integreatly/v1alpha1/pluginsList.go @@ -0,0 +1,41 @@ +package v1alpha1 + +type PluginList []GrafanaPlugin + +// Returns true if the list contains the same plugin in the exact or a different version +func (l PluginList) HasSomeVersionOf(plugin *GrafanaPlugin) bool { + for _, listedPlugin := range l { + if listedPlugin.Name == plugin.Name { + return true + } + } + return false +} + +// Returns true if the list contains the same plugin in the same version +func (l PluginList) HasExactVersionOf(plugin *GrafanaPlugin) bool { + for _, listedPlugin := range l { + if listedPlugin.Name == plugin.Name && listedPlugin.Version == plugin.Version { + return true + } + } + return false +} + +// Returns the number of different versions of a given plugin in the list +func (l PluginList) VersionsOf(plugin *GrafanaPlugin) int { + i := 0 + for _, listedPlugin := range l { + if listedPlugin.Name == plugin.Name { + i = i + 1 + } + } + return i +} + +// Returns the number of different versions of a given plugin in the list +func (l PluginList) SetOrigin(dashboard *GrafanaDashboard) { + for i := range l { + l[i].Origin = dashboard + } +} diff --git a/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go index 555f8c1bc..c5816d5f3 100644 --- a/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/integreatly/v1alpha1/zz_generated.deepcopy.go @@ -1,21 +1,5 @@ // +build !ignore_autogenerated -/* -Copyright The Kubernetes Authors. - -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. -*/ - // Code generated by deepcopy-gen. DO NOT EDIT. package v1alpha1 @@ -59,7 +43,7 @@ func (in *GrafanaDashboard) DeepCopyInto(out *GrafanaDashboard) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -119,8 +103,10 @@ func (in *GrafanaDashboardSpec) DeepCopyInto(out *GrafanaDashboardSpec) { *out = *in if in.Plugins != nil { in, out := &in.Plugins, &out.Plugins - *out = make([]GrafanaPlugin, len(*in)) - copy(*out, *in) + *out = make(PluginList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } @@ -138,6 +124,11 @@ func (in *GrafanaDashboardSpec) DeepCopy() *GrafanaDashboardSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaDashboardStatus) DeepCopyInto(out *GrafanaDashboardStatus) { *out = *in + if in.Messages != nil { + in, out := &in.Messages, &out.Messages + *out = make([]GrafanaDashboardStatusMessage, len(*in)) + copy(*out, *in) + } return } @@ -151,6 +142,22 @@ func (in *GrafanaDashboardStatus) DeepCopy() *GrafanaDashboardStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GrafanaDashboardStatusMessage) DeepCopyInto(out *GrafanaDashboardStatusMessage) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaDashboardStatusMessage. +func (in *GrafanaDashboardStatusMessage) DeepCopy() *GrafanaDashboardStatusMessage { + if in == nil { + return nil + } + out := new(GrafanaDashboardStatusMessage) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaList) DeepCopyInto(out *GrafanaList) { *out = *in @@ -187,6 +194,11 @@ func (in *GrafanaList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GrafanaPlugin) DeepCopyInto(out *GrafanaPlugin) { *out = *in + if in.Origin != nil { + in, out := &in.Origin, &out.Origin + *out = new(GrafanaDashboard) + (*in).DeepCopyInto(*out) + } return } @@ -226,8 +238,10 @@ func (in *GrafanaStatus) DeepCopyInto(out *GrafanaStatus) { *out = *in if in.InstalledPlugins != nil { in, out := &in.InstalledPlugins, &out.InstalledPlugins - *out = make([]GrafanaPlugin, len(*in)) - copy(*out, *in) + *out = make(PluginList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } @@ -241,3 +255,25 @@ func (in *GrafanaStatus) DeepCopy() *GrafanaStatus { in.DeepCopyInto(out) return out } + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in PluginList) DeepCopyInto(out *PluginList) { + { + in := &in + *out = make(PluginList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginList. +func (in PluginList) DeepCopy() PluginList { + if in == nil { + return nil + } + out := new(PluginList) + in.DeepCopyInto(out) + return *out +} diff --git a/pkg/apis/integreatly/v1alpha1/zz_generated.defaults.go b/pkg/apis/integreatly/v1alpha1/zz_generated.defaults.go new file mode 100644 index 000000000..7985166a6 --- /dev/null +++ b/pkg/apis/integreatly/v1alpha1/zz_generated.defaults.go @@ -0,0 +1,16 @@ +// +build !ignore_autogenerated + +// Code generated by defaulter-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// RegisterDefaults adds defaulters functions to the given scheme. +// Public to allow building arbitrary schemes. +// All generated defaulters are covering - they call all nested defaulters. +func RegisterDefaults(scheme *runtime.Scheme) error { + return nil +} diff --git a/pkg/controller/grafana/grafana_controller.go b/pkg/controller/grafana/grafana_controller.go index 7fd853dd1..20e3925af 100644 --- a/pkg/controller/grafana/grafana_controller.go +++ b/pkg/controller/grafana/grafana_controller.go @@ -9,7 +9,6 @@ import ( "time" integreatly "github.com/integr8ly/grafana-operator/pkg/apis/integreatly/v1alpha1" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -43,9 +42,9 @@ func Add(mgr manager.Manager) error { // newReconciler returns a new reconcile.Reconciler func newReconciler(mgr manager.Manager) reconcile.Reconciler { return &ReconcileGrafana{ - client: mgr.GetClient(), - scheme: mgr.GetScheme(), - helper: newKubeHelper(), + client: mgr.GetClient(), + scheme: mgr.GetScheme(), + helper: newKubeHelper(), plugins: newPluginsHelper(), } } @@ -63,17 +62,6 @@ func add(mgr manager.Manager, r reconcile.Reconciler) error { if err != nil { return err } - - // TODO(user): Modify this to be the types you create that are owned by the primary resource - // Watch for changes to secondary resource Pods and requeue the owner Grafana - err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{ - IsController: true, - OwnerType: &integreatly.Grafana{}, - }) - if err != nil { - return err - } - return nil } @@ -83,23 +71,15 @@ var _ reconcile.Reconciler = &ReconcileGrafana{} type ReconcileGrafana struct { // This client, initialized using mgr.Client() above, is a split client // that reads objects from the cache and writes to the apiserver - client client.Client - scheme *runtime.Scheme - helper *KubeHelperImpl + client client.Client + scheme *runtime.Scheme + helper *KubeHelperImpl plugins *PluginsHelperImpl } // Reconcile reads that state of the cluster for a Grafana object and makes changes based on the state read // and what is in the Grafana.Spec -// TODO(user): Modify this Reconcile function to implement your Controller logic. This example creates -// a Pod as an example -// Note: -// The Controller will requeue the Request to be processed again if the returned error is non-nil or -// Result.Requeue is true, otherwise upon completion it will remove the work from the queue. func (r *ReconcileGrafana) Reconcile(request reconcile.Request) (reconcile.Result, error) { - reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name) - reqLogger.Info("Reconciling Grafana") - // Fetch the Grafana instance instance := &integreatly.Grafana{} err := r.client.Get(context.TODO(), request.NamespacedName, instance) @@ -136,73 +116,71 @@ func (r *ReconcileGrafana) ReconcileNamespaces(cr *integreatly.Grafana) (reconci } if len(namespaces) >= 1 { + var requestedPlugins integreatly.PluginList + for _, ns := range namespaces { - log.Info(fmt.Sprintf("Checking namespace %s for dashboards", ns.Name)) dashboards, err := r.helper.getNamespaceDashboards(ns.Name) - if err != nil { - log.Error(err, "Error listing dashboards in namespace") + return reconcile.Result{}, err } else { if len(dashboards.Items) >= 1 { for _, d := range dashboards.Items { - r.ReconcileDashboards(cr, d) + dashboardCopy := d.DeepCopy() + dashboardCopy.Spec.Plugins.SetOrigin(dashboardCopy) + requestedPlugins = append(requestedPlugins, dashboardCopy.Spec.Plugins...) + r.ReconcileDashboards(cr, dashboardCopy) } } } } - } else { - log.Info("No monitoring namespaces, nothing to do") + + filteredPlugins, updated := r.plugins.FilterPlugins(cr, requestedPlugins) + if updated { + r.ReconcilePlugins(cr, filteredPlugins) + } } return reconcile.Result{RequeueAfter: time.Second * 10}, nil } -func (r *ReconcileGrafana) ReconcileDashboards(cr *integreatly.Grafana, d integreatly.GrafanaDashboard) { - log.Info(fmt.Sprintf("Reconciling dashboard: %s", d.Name)) - err := r.helper.updateDashboard(cr.Namespace, d.Namespace, &d) - if err != nil { - log.Error(err, "Error updating dashboard config") - } - err = r.ReconcilePlugins(cr, d) +func (r *ReconcileGrafana) ReconcileDashboards(cr *integreatly.Grafana, d *integreatly.GrafanaDashboard) { + err := r.helper.updateDashboard(cr.Namespace, d.Namespace, d) if err != nil { - log.Error(err, "Error reconciling grafana plugins") + log.Error(err, "Error updating dashboard config") } } -func (r *ReconcileGrafana) ReconcilePlugins(cr *integreatly.Grafana, d integreatly.GrafanaDashboard) error { - if len(d.Spec.Plugins) > 0 { - pluginsAdded := false - for _, plugin := range d.Spec.Plugins { - log.Info(fmt.Sprintf("Processing requested plugin %s", plugin.Name)) - if r.plugins.pluginInstalled(plugin, cr) { - log.Info(fmt.Sprintf("%s is already installed", plugin.Name)) - continue - } +func (r *ReconcileGrafana) ReconcilePlugins(cr *integreatly.Grafana, plugins []integreatly.GrafanaPlugin) error { + var validPlugins []integreatly.GrafanaPlugin + for _, plugin := range plugins { + if r.plugins.PluginExists(plugin) == false { + continue + } - if r.plugins.pluginExists(plugin) == false { - log.Info(fmt.Sprintf("Unknown plugin %s", plugin.Name)) - continue - } + log.Info(fmt.Sprintf("Installing plugin: %s@%s", plugin.Name, plugin.Version)) + validPlugins = append(validPlugins, plugin) + } - log.Info("Adding new plugin %s", plugin.Name) - cr.Status.InstalledPlugins = append(cr.Status.InstalledPlugins, plugin) - pluginsAdded = true - } + cr.Status.InstalledPlugins = validPlugins + err := r.client.Update(context.TODO(), cr) + if err != nil { + return err + } - if pluginsAdded { - err := r.client.Update(context.TODO(), cr) - if err != nil { - return err - } + newEnv := r.plugins.BuildEnv(cr) + err = r.helper.updateGrafanaDeployment(cr.Namespace, newEnv) + if err != nil { + return err + } - newEnv := r.plugins.buildEnv(cr) - err = r.helper.updateGrafanaDeployment(cr.Namespace, newEnv) - return err - } + for _, plugin := range plugins { + log.Info(fmt.Sprintf("== Updating dashboard for plugin")) + log.Info(fmt.Sprintf("%v", plugin.Origin.Status)) + r.ReconcileDashboards(cr, plugin.Origin) } - return nil + return err } func (r *ReconcileGrafana) CreateConfigFiles(cr *integreatly.Grafana) (reconcile.Result, error) { diff --git a/pkg/controller/grafana/kubeHelper.go b/pkg/controller/grafana/kubeHelper.go index fec97d87e..899ae5332 100644 --- a/pkg/controller/grafana/kubeHelper.go +++ b/pkg/controller/grafana/kubeHelper.go @@ -10,6 +10,10 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/config" ) +const ( + InitContainerName = "grafana-plugins-init" +) + type KubeHelperImpl struct { k8client *kubernetes.Clientset grclient *gr.Clientset @@ -74,13 +78,7 @@ func (h KubeHelperImpl) updateDashboard(monitoringNamespace string, dashboardNam } configMap.Data[dashboard.Spec.Name] = dashboard.Spec.Json - _, err = h.k8client.CoreV1().ConfigMaps(monitoringNamespace).Update(configMap) - - if err == nil { - dashboard.Status.Created = true - _, err = h.grclient.IntegreatlyV1alpha1().GrafanaDashboards(dashboardNamespace).Update(dashboard) - } - + configMap, err = h.k8client.CoreV1().ConfigMaps(monitoringNamespace).Update(configMap) return err } @@ -89,19 +87,25 @@ func (h KubeHelperImpl) getGrafanaDeployment(namespaceName string) (*apps.Deploy return h.k8client.AppsV1().Deployments(namespaceName).Get(GrafanaDeploymentName, opts) } - func (h KubeHelperImpl) updateGrafanaDeployment(monitoringNamespace string, newEnv string) error { deployment, err := h.getGrafanaDeployment(monitoringNamespace) if err != nil { return err } + // Leave the deployment alone when it's busy with another operation + if deployment.Status.Replicas != deployment.Status.ReadyReplicas { + return nil + } + updated := false - for _, container := range deployment.Spec.Template.Spec.Containers { - if container.Name == "grafana" { - for _, env := range container.Env { + + // find and update the init container env var + for i, container := range deployment.Spec.Template.Spec.InitContainers { + if container.Name == InitContainerName { + for j, env := range deployment.Spec.Template.Spec.InitContainers[i].Env { if env.Name == PluginsEnvVar { - env.Value = newEnv + deployment.Spec.Template.Spec.InitContainers[i].Env[j].Value = newEnv updated = true break } diff --git a/pkg/controller/grafana/pluginsHelper.go b/pkg/controller/grafana/pluginsHelper.go index d8afcf2b1..1b55c6afb 100644 --- a/pkg/controller/grafana/pluginsHelper.go +++ b/pkg/controller/grafana/pluginsHelper.go @@ -5,6 +5,7 @@ import ( integreatly "github.com/integr8ly/grafana-operator/pkg/apis/integreatly/v1alpha1" "net/http" "strings" + "time" ) const ( @@ -24,7 +25,7 @@ func newPluginsHelper() *PluginsHelperImpl { // Query the Grafana plugin database for the given plugin and version // A 200 OK response indicates that the plugin exists and can be downloaded -func (h *PluginsHelperImpl) pluginExists(plugin integreatly.GrafanaPlugin) bool { +func (h *PluginsHelperImpl) PluginExists(plugin integreatly.GrafanaPlugin) bool { url := fmt.Sprintf(h.BaseUrl, plugin.Name, plugin.Version) resp, err := http.Get(url) if err != nil { @@ -42,7 +43,7 @@ func (h *PluginsHelperImpl) pluginExists(plugin integreatly.GrafanaPlugin) bool // Turns an array of plugins into a string representation of the form // `:,...` that is used as the value for the GRAFANA_PLUGINS // environment variable -func (h *PluginsHelperImpl) buildEnv(cr *integreatly.Grafana) string { +func (h *PluginsHelperImpl) BuildEnv(cr *integreatly.Grafana) string { var env []string for _, plugin := range cr.Status.InstalledPlugins { env = append(env, fmt.Sprintf("%s:%s", plugin.Name, plugin.Version)) @@ -50,13 +51,66 @@ func (h *PluginsHelperImpl) buildEnv(cr *integreatly.Grafana) string { return strings.Join(env, ",") } -// Checks if a given plugin is already installed. We do not allow to install -// multiple versions of the same plugin -func (h *PluginsHelperImpl) pluginInstalled(plugin integreatly.GrafanaPlugin, cr *integreatly.Grafana) bool { - for _, installedPlugin := range cr.Status.InstalledPlugins { - if installedPlugin.Name == plugin.Name { - return true +func (h *PluginsHelperImpl) AppendMessage(message string, d *integreatly.GrafanaDashboard) { + status := integreatly.GrafanaDashboardStatusMessage{ + Message: message, + Timestamp: time.Now().Format(time.RFC850), + } + + d.Status.Messages = append(d.Status.Messages, status) +} + +// Creates the list of plugins that can be added or updated +// Does not directly deal with removing plugins: if a plugin is not in the list and the env var is updated, it will +// automatically be removed +func (h *PluginsHelperImpl) FilterPlugins(cr *integreatly.Grafana, requested integreatly.PluginList) (integreatly.PluginList, bool) { + filteredPlugins := integreatly.PluginList{} + pluginsUpdated := false + + // Remove all plugins + if len(requested) == 0 && len(cr.Status.InstalledPlugins) > 0 { + return filteredPlugins, true + } + + for _, plugin := range requested { + // Don't allow to install multiple versions of the same plugin + if filteredPlugins.HasSomeVersionOf(&plugin) == true { + h.AppendMessage(fmt.Sprintf("Another version of %s is already installed", plugin.Name), plugin.Origin) + continue + } + + // Already installed: append it to the list to keep it + if cr.Status.InstalledPlugins.HasExactVersionOf(&plugin) { + filteredPlugins = append(filteredPlugins, plugin) + continue + } + + // New plugin + if cr.Status.InstalledPlugins.HasSomeVersionOf(&plugin) == false { + filteredPlugins = append(filteredPlugins, plugin) + h.AppendMessage(fmt.Sprintf("Installing plugin %s@%s", plugin.Name, plugin.Version), plugin.Origin) + pluginsUpdated = true + continue + } + + // Plugin update: allow to update a plugin if only one dashboard requests it + // If multiple dashboards request different versions of the same plugin, then we can't upgrade because + // there is no way to decide which version is the correct one + if cr.Status.InstalledPlugins.HasSomeVersionOf(&plugin) == true && + cr.Status.InstalledPlugins.HasExactVersionOf(&plugin) == false && + requested.VersionsOf(&plugin) == 1 { + filteredPlugins = append(filteredPlugins, plugin) + pluginsUpdated = true + continue } } - return false + + // Check for removed plugins + for _, plugin := range cr.Status.InstalledPlugins { + if requested.HasSomeVersionOf(&plugin) == false { + pluginsUpdated = true + } + } + + return filteredPlugins, pluginsUpdated } diff --git a/templates/grafana-deployment.yaml b/templates/grafana-deployment.yaml index 49941b657..b3d6603da 100644 --- a/templates/grafana-deployment.yaml +++ b/templates/grafana-deployment.yaml @@ -23,11 +23,21 @@ spec: app: grafana name: grafana spec: + initContainers: + - env: + - name: GRAFANA_PLUGINS + value: "" + image: 'docker.io/pb82/grafana_plugins_init:latest' + imagePullPolicy: IfNotPresent + name: grafana-plugins-init + resources: {} + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /opt/plugins + name: grafana-plugins containers: - image: '{{ .GrafanaImage }}:{{ .GrafanaVersion }}' - env: - - name: GRAFANA_PLUGINS - - value: "" args: - -config=/etc/grafana/grafana.ini imagePullPolicy: IfNotPresent From 70f0ae3d52eddefb2101a351c8ab28a10d5ddbf1 Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Wed, 17 Apr 2019 11:42:55 +0200 Subject: [PATCH 4/5] add test and semver dependency --- Gopkg.lock | 9 + Gopkg.toml | 4 + Makefile | 4 +- pkg/apis/integreatly/v1alpha1/pluginsList.go | 40 +- pkg/controller/grafana/grafana_controller.go | 29 +- pkg/controller/grafana/pluginsHelper.go | 43 +- pkg/controller/grafana/pluginsHelper_test.go | 88 ++++ pkg/controller/grafana/testing_shared.go | 43 ++ vendor/github.com/blang/semver/LICENSE | 22 + .../github.com/blang/semver/examples/main.go | 84 ++++ vendor/github.com/blang/semver/json.go | 23 + vendor/github.com/blang/semver/range.go | 416 ++++++++++++++++ vendor/github.com/blang/semver/semver.go | 455 ++++++++++++++++++ vendor/github.com/blang/semver/sort.go | 28 ++ vendor/github.com/blang/semver/sql.go | 30 ++ 15 files changed, 1299 insertions(+), 19 deletions(-) create mode 100644 pkg/controller/grafana/pluginsHelper_test.go create mode 100644 pkg/controller/grafana/testing_shared.go create mode 100644 vendor/github.com/blang/semver/LICENSE create mode 100644 vendor/github.com/blang/semver/examples/main.go create mode 100644 vendor/github.com/blang/semver/json.go create mode 100644 vendor/github.com/blang/semver/range.go create mode 100644 vendor/github.com/blang/semver/semver.go create mode 100644 vendor/github.com/blang/semver/sort.go create mode 100644 vendor/github.com/blang/semver/sql.go diff --git a/Gopkg.lock b/Gopkg.lock index e2cafbb8b..ec8652458 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -25,6 +25,14 @@ pruneopts = "NT" revision = "de5bf2ad457846296e2031421a34e2568e304e35" +[[projects]] + digest = "1:dafc9bfdb2d9816587ca63844a722b36525183e14423b1f356fd9a8a805b6230" + name = "github.com/blang/semver" + packages = ["."] + pruneopts = "NT" + revision = "ba2c2ddd89069b46a7011d4106f6868f17ee1705" + version = "v3.6.1" + [[projects]] digest = "1:4b8b5811da6970495e04d1f4e98bb89518cc3cfc3b3f456bdb876ed7b6c74049" name = "github.com/davecgh/go-spew" @@ -737,6 +745,7 @@ analyzer-name = "dep" analyzer-version = 1 input-imports = [ + "github.com/blang/semver", "github.com/ghodss/yaml", "github.com/operator-framework/operator-sdk/pkg/k8sutil", "github.com/operator-framework/operator-sdk/pkg/leader", diff --git a/Gopkg.toml b/Gopkg.toml index cbc6a9821..feb6a0f64 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -45,6 +45,10 @@ required = [ # branch = "v0.2.x" #osdk_branch_annotation version = "=v0.2.1" #osdk_version_annotation +[[constraint]] + name = "github.com/blang/semver" + version = "v3.6.1" + [prune] go-tests = true non-go = true diff --git a/Makefile b/Makefile index 0c7e683d6..83251dc31 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ -ORG=pb82 +ORG=integr8ly NAMESPACE=application-monitoring PROJECT=grafana-operator -REG=docker.io +REG=quay.io SHELL=/bin/bash TAG=latest PKG=github.com/integr8ly/grafana-operator diff --git a/pkg/apis/integreatly/v1alpha1/pluginsList.go b/pkg/apis/integreatly/v1alpha1/pluginsList.go index 94cfecea3..c6d3618b6 100644 --- a/pkg/apis/integreatly/v1alpha1/pluginsList.go +++ b/pkg/apis/integreatly/v1alpha1/pluginsList.go @@ -1,5 +1,9 @@ package v1alpha1 +import ( + "github.com/blang/semver" +) + type PluginList []GrafanaPlugin // Returns true if the list contains the same plugin in the exact or a different version @@ -12,6 +16,16 @@ func (l PluginList) HasSomeVersionOf(plugin *GrafanaPlugin) bool { return false } +// Get the plugin from the list regardless of the version +func (l PluginList) GetInstalledVersionOf(plugin *GrafanaPlugin) *GrafanaPlugin { + for _, listedPlugin := range l { + if listedPlugin.Name == plugin.Name { + return &listedPlugin + } + } + return nil +} + // Returns true if the list contains the same plugin in the same version func (l PluginList) HasExactVersionOf(plugin *GrafanaPlugin) bool { for _, listedPlugin := range l { @@ -22,6 +36,30 @@ func (l PluginList) HasExactVersionOf(plugin *GrafanaPlugin) bool { return false } +// Returns true if the list contains the same plugin but in a newer version +func (l PluginList) HasNewerVersionOf(plugin *GrafanaPlugin) (bool, error) { + for _, listedPlugin := range l { + if listedPlugin.Name != plugin.Name { + continue + } + + listedVersion, err := semver.Make(listedPlugin.Version) + if err != nil { + return false, err + } + + requestedVersion, err := semver.Make(plugin.Version) + if err != nil { + return false, err + } + + if listedVersion.Compare(requestedVersion) == 1 { + return true, nil + } + } + return false, nil +} + // Returns the number of different versions of a given plugin in the list func (l PluginList) VersionsOf(plugin *GrafanaPlugin) int { i := 0 @@ -33,7 +71,7 @@ func (l PluginList) VersionsOf(plugin *GrafanaPlugin) int { return i } -// Returns the number of different versions of a given plugin in the list +// Set the originating dashboard for every plugin in the list func (l PluginList) SetOrigin(dashboard *GrafanaDashboard) { for i := range l { l[i].Origin = dashboard diff --git a/pkg/controller/grafana/grafana_controller.go b/pkg/controller/grafana/grafana_controller.go index 20e3925af..0f58fe860 100644 --- a/pkg/controller/grafana/grafana_controller.go +++ b/pkg/controller/grafana/grafana_controller.go @@ -128,7 +128,7 @@ func (r *ReconcileGrafana) ReconcileNamespaces(cr *integreatly.Grafana) (reconci dashboardCopy := d.DeepCopy() dashboardCopy.Spec.Plugins.SetOrigin(dashboardCopy) requestedPlugins = append(requestedPlugins, dashboardCopy.Spec.Plugins...) - r.ReconcileDashboards(cr, dashboardCopy) + r.ReconcileDashboardConfigMap(cr, dashboardCopy) } } } @@ -137,14 +137,27 @@ func (r *ReconcileGrafana) ReconcileNamespaces(cr *integreatly.Grafana) (reconci filteredPlugins, updated := r.plugins.FilterPlugins(cr, requestedPlugins) if updated { r.ReconcilePlugins(cr, filteredPlugins) + + // Update the dashboards that had their plugins modified + // to let the owners know about the status + err = r.ReconcileDashboards(filteredPlugins) } } - return reconcile.Result{RequeueAfter: time.Second * 10}, nil + return reconcile.Result{RequeueAfter: time.Second * 10}, err } +func (r *ReconcileGrafana) ReconcileDashboards(plugins integreatly.PluginList) error { + for _, plugin := range plugins { + err := r.client.Update(context.TODO(), plugin.Origin) + if err != nil { + return err + } + } + return nil +} -func (r *ReconcileGrafana) ReconcileDashboards(cr *integreatly.Grafana, d *integreatly.GrafanaDashboard) { +func (r *ReconcileGrafana) ReconcileDashboardConfigMap(cr *integreatly.Grafana, d *integreatly.GrafanaDashboard) { err := r.helper.updateDashboard(cr.Namespace, d.Namespace, d) if err != nil { log.Error(err, "Error updating dashboard config") @@ -170,16 +183,6 @@ func (r *ReconcileGrafana) ReconcilePlugins(cr *integreatly.Grafana, plugins []i newEnv := r.plugins.BuildEnv(cr) err = r.helper.updateGrafanaDeployment(cr.Namespace, newEnv) - if err != nil { - return err - } - - for _, plugin := range plugins { - log.Info(fmt.Sprintf("== Updating dashboard for plugin")) - log.Info(fmt.Sprintf("%v", plugin.Origin.Status)) - r.ReconcileDashboards(cr, plugin.Origin) - } - return err } diff --git a/pkg/controller/grafana/pluginsHelper.go b/pkg/controller/grafana/pluginsHelper.go index 1b55c6afb..20745d869 100644 --- a/pkg/controller/grafana/pluginsHelper.go +++ b/pkg/controller/grafana/pluginsHelper.go @@ -51,13 +51,39 @@ func (h *PluginsHelperImpl) BuildEnv(cr *integreatly.Grafana) string { return strings.Join(env, ",") } -func (h *PluginsHelperImpl) AppendMessage(message string, d *integreatly.GrafanaDashboard) { +// Append a status message to the origin dashboard of a plugin +func (h *PluginsHelperImpl) AppendMessage(message string, dashboard *integreatly.GrafanaDashboard) { + if dashboard == nil { + return + } + status := integreatly.GrafanaDashboardStatusMessage{ Message: message, Timestamp: time.Now().Format(time.RFC850), } - d.Status.Messages = append(d.Status.Messages, status) + dashboard.Status.Messages = append(dashboard.Status.Messages, status) +} + +// Append a status message to the origin dashboard of a plugin +func (h *PluginsHelperImpl) PickLatestVersions(requested integreatly.PluginList) (integreatly.PluginList, error) { + var latestVersions integreatly.PluginList + for _, plugin := range requested { + result, err := requested.HasNewerVersionOf(&plugin) + + // Errors might happen if plugins don't use semver + // In that case fall back to whichever comes first + if err != nil { + return requested, err + } + + // Skip this version if there is a more recent one + if result { + continue + } + latestVersions = append(latestVersions, plugin) + } + return latestVersions, nil } // Creates the list of plugins that can be added or updated @@ -67,6 +93,12 @@ func (h *PluginsHelperImpl) FilterPlugins(cr *integreatly.Grafana, requested int filteredPlugins := integreatly.PluginList{} pluginsUpdated := false + // Try to pick the latest versions of all plugins + requested, err := h.PickLatestVersions(requested) + if err != nil { + log.Error(err, "Unable to pick latest plugin versions") + } + // Remove all plugins if len(requested) == 0 && len(cr.Status.InstalledPlugins) > 0 { return filteredPlugins, true @@ -75,7 +107,8 @@ func (h *PluginsHelperImpl) FilterPlugins(cr *integreatly.Grafana, requested int for _, plugin := range requested { // Don't allow to install multiple versions of the same plugin if filteredPlugins.HasSomeVersionOf(&plugin) == true { - h.AppendMessage(fmt.Sprintf("Another version of %s is already installed", plugin.Name), plugin.Origin) + installedVersion := filteredPlugins.GetInstalledVersionOf(&plugin) + h.AppendMessage(fmt.Sprintf("Not installing version %s of %s because %s is already installed", plugin.Version, plugin.Name, installedVersion.Version), plugin.Origin) continue } @@ -94,12 +127,16 @@ func (h *PluginsHelperImpl) FilterPlugins(cr *integreatly.Grafana, requested int } // Plugin update: allow to update a plugin if only one dashboard requests it + // The condition is: some version of the plugin is aleady installed, but it's not the exact same version + // and there is only one dashboard that requires this plugin // If multiple dashboards request different versions of the same plugin, then we can't upgrade because // there is no way to decide which version is the correct one if cr.Status.InstalledPlugins.HasSomeVersionOf(&plugin) == true && cr.Status.InstalledPlugins.HasExactVersionOf(&plugin) == false && requested.VersionsOf(&plugin) == 1 { + installedVersion := cr.Status.InstalledPlugins.GetInstalledVersionOf(&plugin) filteredPlugins = append(filteredPlugins, plugin) + h.AppendMessage(fmt.Sprintf("Changing version of plugin %s form %s to %s", plugin.Name, installedVersion.Version, plugin.Version), plugin.Origin) pluginsUpdated = true continue } diff --git a/pkg/controller/grafana/pluginsHelper_test.go b/pkg/controller/grafana/pluginsHelper_test.go new file mode 100644 index 000000000..aab16024d --- /dev/null +++ b/pkg/controller/grafana/pluginsHelper_test.go @@ -0,0 +1,88 @@ +package grafana + +import ( + "testing" +) + +func TestPluginsList(t *testing.T) { + result := true + result = result && MockPluginList.HasSomeVersionOf(&Mockplugina100) + result = result && MockPluginList.HasSomeVersionOf(&Mockplugina101) + result = result && MockPluginList.HasSomeVersionOf(&Mockpluginb100) + result = result && MockPluginList.HasSomeVersionOf(&Mockplugina102) + + if !result { + t.Errorf("Error in `HasSomeVersionOf`") + } + + result = result && MockPluginList.HasExactVersionOf(&Mockplugina100) + result = result && MockPluginList.HasExactVersionOf(&Mockplugina101) + result = result && MockPluginList.HasExactVersionOf(&Mockpluginb100) + result = result && (MockPluginList.HasExactVersionOf(&Mockplugina102) == false) + result = result && (MockPluginList.HasExactVersionOf(&Mockpluginc100) == false) + + if !result { + t.Errorf("Error in `HasExactVersionOf`") + } + + result, err := MockPluginList.HasNewerVersionOf(&Mockplugina100) + if err != nil { + t.Error(err) + } + + if !result { + t.Errorf("Error in `HasNewerVersionOf`") + } + + result, err = MockPluginList.HasNewerVersionOf(&Mockplugina101) + if err != nil { + t.Error(err) + } + + if result { + t.Errorf("Error in `HasNewerVersionOf`") + } +} + +func TestPluginsHelperImpl_PickLatestVersions(t *testing.T) { + var h PluginsHelperImpl + + latestVersions, err := h.PickLatestVersions(MockPluginList) + if err != nil { + t.Error(err) + } + + if latestVersions.HasExactVersionOf(&Mockplugina100) { + t.Errorf("Expected %s but got %s", Mockplugina101.Version, Mockplugina100.Version) + } + + result, err := latestVersions.HasNewerVersionOf(&Mockplugina101) + if err != nil { + t.Error(err) + } + + if result { + t.Errorf("Expected no newer version than %s", Mockplugina101.Version) + } +} + +func TestPluginsHelperImpl_FilterPlugins(t *testing.T) { + var h PluginsHelperImpl + + MockPluginList.SetOrigin(&MockDashboard) + installed, updated := h.FilterPlugins(&MockGrafana, MockPluginList) + + if !updated { + t.Errorf("Expected plugins to be installed") + } + + result := true + result = result && installed.HasExactVersionOf(&Mockplugina101) + result = result && installed.HasExactVersionOf(&Mockpluginb100) + result = result && (len(MockDashboard.Status.Messages) == 2) + result = result && (len(installed) == 2) + + if !result { + t.Errorf("Unexpected plugins got installed") + } +} diff --git a/pkg/controller/grafana/testing_shared.go b/pkg/controller/grafana/testing_shared.go new file mode 100644 index 000000000..ab5e11ef3 --- /dev/null +++ b/pkg/controller/grafana/testing_shared.go @@ -0,0 +1,43 @@ +package grafana + +import "github.com/integr8ly/grafana-operator/pkg/apis/integreatly/v1alpha1" + +var Mockplugina100 = v1alpha1.GrafanaPlugin{ + Name: "a", + Version: "1.0.0", +} + +var Mockplugina101 = v1alpha1.GrafanaPlugin{ + Name: "a", + Version: "1.0.1", +} + +var Mockplugina102 = v1alpha1.GrafanaPlugin{ + Name: "a", + Version: "1.0.2", +} + +var Mockpluginb100 = v1alpha1.GrafanaPlugin{ + Name: "b", + Version: "1.0.0", +} + +var Mockpluginc100 = v1alpha1.GrafanaPlugin{ + Name: "c", + Version: "1.0.0", +} + +var MockPluginList = v1alpha1.PluginList{Mockplugina100, Mockplugina101, Mockpluginb100} + +var MockDashboard = v1alpha1.GrafanaDashboard{ + Status: v1alpha1.GrafanaDashboardStatus{ + Messages: []v1alpha1.GrafanaDashboardStatusMessage{}, + }, +} + +var MockGrafana = v1alpha1.Grafana{ + Status: v1alpha1.GrafanaStatus{ + Phase: 0, + InstalledPlugins: v1alpha1.PluginList{}, + }, +} diff --git a/vendor/github.com/blang/semver/LICENSE b/vendor/github.com/blang/semver/LICENSE new file mode 100644 index 000000000..5ba5c86fc --- /dev/null +++ b/vendor/github.com/blang/semver/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2014 Benedikt Lang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/vendor/github.com/blang/semver/examples/main.go b/vendor/github.com/blang/semver/examples/main.go new file mode 100644 index 000000000..7bd1c551d --- /dev/null +++ b/vendor/github.com/blang/semver/examples/main.go @@ -0,0 +1,84 @@ +package main + +import ( + "fmt" + + "github.com/blang/semver" +) + +func main() { + v, err := semver.Parse("0.0.1-alpha.preview.222+123.github") + if err != nil { + fmt.Printf("Error while parsing (not valid): %q", err) + } + fmt.Printf("Version to string: %q\n", v) + + fmt.Printf("Major: %d\n", v.Major) + fmt.Printf("Minor: %d\n", v.Minor) + fmt.Printf("Patch: %d\n", v.Patch) + + // Prerelease versions + if len(v.Pre) > 0 { + fmt.Println("Prerelease versions:") + for i, pre := range v.Pre { + fmt.Printf("%d: %q\n", i, pre) + } + } + + // Build meta data + if len(v.Build) > 0 { + fmt.Println("Build meta data:") + for i, build := range v.Build { + fmt.Printf("%d: %q\n", i, build) + } + } + + // Make == Parse (Value), New for Pointer + v001, _ := semver.Make("0.0.1") + + fmt.Println("\nUse Version.Compare for comparisons (-1, 0, 1):") + fmt.Printf("%q is greater than %q: Compare == %d\n", v001, v, v001.Compare(v)) + fmt.Printf("%q is less than %q: Compare == %d\n", v, v001, v.Compare(v001)) + fmt.Printf("%q is equal to %q: Compare == %d\n", v, v, v.Compare(v)) + + fmt.Println("\nUse comparison helpers returning booleans:") + fmt.Printf("%q is greater than %q: %t\n", v001, v, v001.GT(v)) + fmt.Printf("%q is greater than equal %q: %t\n", v001, v, v001.GTE(v)) + fmt.Printf("%q is greater than equal %q: %t\n", v, v, v.GTE(v)) + fmt.Printf("%q is less than %q: %t\n", v, v001, v.LT(v001)) + fmt.Printf("%q is less than equal %q: %t\n", v, v001, v.LTE(v001)) + fmt.Printf("%q is less than equal %q: %t\n", v, v, v.LTE(v)) + + fmt.Println("\nManipulate Version in place:") + v.Pre[0], err = semver.NewPRVersion("beta") + if err != nil { + fmt.Printf("Error parsing pre release version: %q", err) + } + fmt.Printf("Version to string: %q\n", v) + + fmt.Println("\nCompare Prerelease versions:") + pre1, _ := semver.NewPRVersion("123") + pre2, _ := semver.NewPRVersion("alpha") + pre3, _ := semver.NewPRVersion("124") + fmt.Printf("%q is less than %q: Compare == %d\n", pre1, pre2, pre1.Compare(pre2)) + fmt.Printf("%q is greater than %q: Compare == %d\n", pre3, pre1, pre3.Compare(pre1)) + fmt.Printf("%q is equal to %q: Compare == %d\n", pre1, pre1, pre1.Compare(pre1)) + + fmt.Println("\nValidate versions:") + v.Build[0] = "?" + + err = v.Validate() + if err != nil { + fmt.Printf("Validation failed: %s\n", err) + } + + fmt.Println("Create valid build meta data:") + b1, _ := semver.NewBuildVersion("build123") + v.Build[0] = b1 + fmt.Printf("Version with new build version %q\n", v) + + _, err = semver.NewBuildVersion("build?123") + if err != nil { + fmt.Printf("Create build version failed: %s\n", err) + } +} diff --git a/vendor/github.com/blang/semver/json.go b/vendor/github.com/blang/semver/json.go new file mode 100644 index 000000000..a74bf7c44 --- /dev/null +++ b/vendor/github.com/blang/semver/json.go @@ -0,0 +1,23 @@ +package semver + +import ( + "encoding/json" +) + +// MarshalJSON implements the encoding/json.Marshaler interface. +func (v Version) MarshalJSON() ([]byte, error) { + return json.Marshal(v.String()) +} + +// UnmarshalJSON implements the encoding/json.Unmarshaler interface. +func (v *Version) UnmarshalJSON(data []byte) (err error) { + var versionString string + + if err = json.Unmarshal(data, &versionString); err != nil { + return + } + + *v, err = Parse(versionString) + + return +} diff --git a/vendor/github.com/blang/semver/range.go b/vendor/github.com/blang/semver/range.go new file mode 100644 index 000000000..95f7139b9 --- /dev/null +++ b/vendor/github.com/blang/semver/range.go @@ -0,0 +1,416 @@ +package semver + +import ( + "fmt" + "strconv" + "strings" + "unicode" +) + +type wildcardType int + +const ( + noneWildcard wildcardType = iota + majorWildcard wildcardType = 1 + minorWildcard wildcardType = 2 + patchWildcard wildcardType = 3 +) + +func wildcardTypefromInt(i int) wildcardType { + switch i { + case 1: + return majorWildcard + case 2: + return minorWildcard + case 3: + return patchWildcard + default: + return noneWildcard + } +} + +type comparator func(Version, Version) bool + +var ( + compEQ comparator = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 0 + } + compNE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) != 0 + } + compGT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == 1 + } + compGE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) >= 0 + } + compLT = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) == -1 + } + compLE = func(v1 Version, v2 Version) bool { + return v1.Compare(v2) <= 0 + } +) + +type versionRange struct { + v Version + c comparator +} + +// rangeFunc creates a Range from the given versionRange. +func (vr *versionRange) rangeFunc() Range { + return Range(func(v Version) bool { + return vr.c(v, vr.v) + }) +} + +// Range represents a range of versions. +// A Range can be used to check if a Version satisfies it: +// +// range, err := semver.ParseRange(">1.0.0 <2.0.0") +// range(semver.MustParse("1.1.1") // returns true +type Range func(Version) bool + +// OR combines the existing Range with another Range using logical OR. +func (rf Range) OR(f Range) Range { + return Range(func(v Version) bool { + return rf(v) || f(v) + }) +} + +// AND combines the existing Range with another Range using logical AND. +func (rf Range) AND(f Range) Range { + return Range(func(v Version) bool { + return rf(v) && f(v) + }) +} + +// ParseRange parses a range and returns a Range. +// If the range could not be parsed an error is returned. +// +// Valid ranges are: +// - "<1.0.0" +// - "<=1.0.0" +// - ">1.0.0" +// - ">=1.0.0" +// - "1.0.0", "=1.0.0", "==1.0.0" +// - "!1.0.0", "!=1.0.0" +// +// A Range can consist of multiple ranges separated by space: +// Ranges can be linked by logical AND: +// - ">1.0.0 <2.0.0" would match between both ranges, so "1.1.1" and "1.8.7" but not "1.0.0" or "2.0.0" +// - ">1.0.0 <3.0.0 !2.0.3-beta.2" would match every version between 1.0.0 and 3.0.0 except 2.0.3-beta.2 +// +// Ranges can also be linked by logical OR: +// - "<2.0.0 || >=3.0.0" would match "1.x.x" and "3.x.x" but not "2.x.x" +// +// AND has a higher precedence than OR. It's not possible to use brackets. +// +// Ranges can be combined by both AND and OR +// +// - `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1` +func ParseRange(s string) (Range, error) { + parts := splitAndTrim(s) + orParts, err := splitORParts(parts) + if err != nil { + return nil, err + } + expandedParts, err := expandWildcardVersion(orParts) + if err != nil { + return nil, err + } + var orFn Range + for _, p := range expandedParts { + var andFn Range + for _, ap := range p { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + vr, err := buildVersionRange(opStr, vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse Range %q: %s", ap, err) + } + rf := vr.rangeFunc() + + // Set function + if andFn == nil { + andFn = rf + } else { // Combine with existing function + andFn = andFn.AND(rf) + } + } + if orFn == nil { + orFn = andFn + } else { + orFn = orFn.OR(andFn) + } + + } + return orFn, nil +} + +// splitORParts splits the already cleaned parts by '||'. +// Checks for invalid positions of the operator and returns an +// error if found. +func splitORParts(parts []string) ([][]string, error) { + var ORparts [][]string + last := 0 + for i, p := range parts { + if p == "||" { + if i == 0 { + return nil, fmt.Errorf("First element in range is '||'") + } + ORparts = append(ORparts, parts[last:i]) + last = i + 1 + } + } + if last == len(parts) { + return nil, fmt.Errorf("Last element in range is '||'") + } + ORparts = append(ORparts, parts[last:]) + return ORparts, nil +} + +// buildVersionRange takes a slice of 2: operator and version +// and builds a versionRange, otherwise an error. +func buildVersionRange(opStr, vStr string) (*versionRange, error) { + c := parseComparator(opStr) + if c == nil { + return nil, fmt.Errorf("Could not parse comparator %q in %q", opStr, strings.Join([]string{opStr, vStr}, "")) + } + v, err := Parse(vStr) + if err != nil { + return nil, fmt.Errorf("Could not parse version %q in %q: %s", vStr, strings.Join([]string{opStr, vStr}, ""), err) + } + + return &versionRange{ + v: v, + c: c, + }, nil + +} + +// inArray checks if a byte is contained in an array of bytes +func inArray(s byte, list []byte) bool { + for _, el := range list { + if el == s { + return true + } + } + return false +} + +// splitAndTrim splits a range string by spaces and cleans whitespaces +func splitAndTrim(s string) (result []string) { + last := 0 + var lastChar byte + excludeFromSplit := []byte{'>', '<', '='} + for i := 0; i < len(s); i++ { + if s[i] == ' ' && !inArray(lastChar, excludeFromSplit) { + if last < i-1 { + result = append(result, s[last:i]) + } + last = i + 1 + } else if s[i] != ' ' { + lastChar = s[i] + } + } + if last < len(s)-1 { + result = append(result, s[last:]) + } + + for i, v := range result { + result[i] = strings.Replace(v, " ", "", -1) + } + + // parts := strings.Split(s, " ") + // for _, x := range parts { + // if s := strings.TrimSpace(x); len(s) != 0 { + // result = append(result, s) + // } + // } + return +} + +// splitComparatorVersion splits the comparator from the version. +// Input must be free of leading or trailing spaces. +func splitComparatorVersion(s string) (string, string, error) { + i := strings.IndexFunc(s, unicode.IsDigit) + if i == -1 { + return "", "", fmt.Errorf("Could not get version from string: %q", s) + } + return strings.TrimSpace(s[0:i]), s[i:], nil +} + +// getWildcardType will return the type of wildcard that the +// passed version contains +func getWildcardType(vStr string) wildcardType { + parts := strings.Split(vStr, ".") + nparts := len(parts) + wildcard := parts[nparts-1] + + possibleWildcardType := wildcardTypefromInt(nparts) + if wildcard == "x" { + return possibleWildcardType + } + + return noneWildcard +} + +// createVersionFromWildcard will convert a wildcard version +// into a regular version, replacing 'x's with '0's, handling +// special cases like '1.x.x' and '1.x' +func createVersionFromWildcard(vStr string) string { + // handle 1.x.x + vStr2 := strings.Replace(vStr, ".x.x", ".x", 1) + vStr2 = strings.Replace(vStr2, ".x", ".0", 1) + parts := strings.Split(vStr2, ".") + + // handle 1.x + if len(parts) == 2 { + return vStr2 + ".0" + } + + return vStr2 +} + +// incrementMajorVersion will increment the major version +// of the passed version +func incrementMajorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[0]) + if err != nil { + return "", err + } + parts[0] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// incrementMajorVersion will increment the minor version +// of the passed version +func incrementMinorVersion(vStr string) (string, error) { + parts := strings.Split(vStr, ".") + i, err := strconv.Atoi(parts[1]) + if err != nil { + return "", err + } + parts[1] = strconv.Itoa(i + 1) + + return strings.Join(parts, "."), nil +} + +// expandWildcardVersion will expand wildcards inside versions +// following these rules: +// +// * when dealing with patch wildcards: +// >= 1.2.x will become >= 1.2.0 +// <= 1.2.x will become < 1.3.0 +// > 1.2.x will become >= 1.3.0 +// < 1.2.x will become < 1.2.0 +// != 1.2.x will become < 1.2.0 >= 1.3.0 +// +// * when dealing with minor wildcards: +// >= 1.x will become >= 1.0.0 +// <= 1.x will become < 2.0.0 +// > 1.x will become >= 2.0.0 +// < 1.0 will become < 1.0.0 +// != 1.x will become < 1.0.0 >= 2.0.0 +// +// * when dealing with wildcards without +// version operator: +// 1.2.x will become >= 1.2.0 < 1.3.0 +// 1.x will become >= 1.0.0 < 2.0.0 +func expandWildcardVersion(parts [][]string) ([][]string, error) { + var expandedParts [][]string + for _, p := range parts { + var newParts []string + for _, ap := range p { + if strings.Contains(ap, "x") { + opStr, vStr, err := splitComparatorVersion(ap) + if err != nil { + return nil, err + } + + versionWildcardType := getWildcardType(vStr) + flatVersion := createVersionFromWildcard(vStr) + + var resultOperator string + var shouldIncrementVersion bool + switch opStr { + case ">": + resultOperator = ">=" + shouldIncrementVersion = true + case ">=": + resultOperator = ">=" + case "<": + resultOperator = "<" + case "<=": + resultOperator = "<" + shouldIncrementVersion = true + case "", "=", "==": + newParts = append(newParts, ">="+flatVersion) + resultOperator = "<" + shouldIncrementVersion = true + case "!=", "!": + newParts = append(newParts, "<"+flatVersion) + resultOperator = ">=" + shouldIncrementVersion = true + } + + var resultVersion string + if shouldIncrementVersion { + switch versionWildcardType { + case patchWildcard: + resultVersion, _ = incrementMinorVersion(flatVersion) + case minorWildcard: + resultVersion, _ = incrementMajorVersion(flatVersion) + } + } else { + resultVersion = flatVersion + } + + ap = resultOperator + resultVersion + } + newParts = append(newParts, ap) + } + expandedParts = append(expandedParts, newParts) + } + + return expandedParts, nil +} + +func parseComparator(s string) comparator { + switch s { + case "==": + fallthrough + case "": + fallthrough + case "=": + return compEQ + case ">": + return compGT + case ">=": + return compGE + case "<": + return compLT + case "<=": + return compLE + case "!": + fallthrough + case "!=": + return compNE + } + + return nil +} + +// MustParseRange is like ParseRange but panics if the range cannot be parsed. +func MustParseRange(s string) Range { + r, err := ParseRange(s) + if err != nil { + panic(`semver: ParseRange(` + s + `): ` + err.Error()) + } + return r +} diff --git a/vendor/github.com/blang/semver/semver.go b/vendor/github.com/blang/semver/semver.go new file mode 100644 index 000000000..4165bc791 --- /dev/null +++ b/vendor/github.com/blang/semver/semver.go @@ -0,0 +1,455 @@ +package semver + +import ( + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + numbers string = "0123456789" + alphas = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + alphanum = alphas + numbers +) + +// SpecVersion is the latest fully supported spec version of semver +var SpecVersion = Version{ + Major: 2, + Minor: 0, + Patch: 0, +} + +// Version represents a semver compatible version +type Version struct { + Major uint64 + Minor uint64 + Patch uint64 + Pre []PRVersion + Build []string //No Precedence +} + +// Version to string +func (v Version) String() string { + b := make([]byte, 0, 5) + b = strconv.AppendUint(b, v.Major, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Minor, 10) + b = append(b, '.') + b = strconv.AppendUint(b, v.Patch, 10) + + if len(v.Pre) > 0 { + b = append(b, '-') + b = append(b, v.Pre[0].String()...) + + for _, pre := range v.Pre[1:] { + b = append(b, '.') + b = append(b, pre.String()...) + } + } + + if len(v.Build) > 0 { + b = append(b, '+') + b = append(b, v.Build[0]...) + + for _, build := range v.Build[1:] { + b = append(b, '.') + b = append(b, build...) + } + } + + return string(b) +} + +// Equals checks if v is equal to o. +func (v Version) Equals(o Version) bool { + return (v.Compare(o) == 0) +} + +// EQ checks if v is equal to o. +func (v Version) EQ(o Version) bool { + return (v.Compare(o) == 0) +} + +// NE checks if v is not equal to o. +func (v Version) NE(o Version) bool { + return (v.Compare(o) != 0) +} + +// GT checks if v is greater than o. +func (v Version) GT(o Version) bool { + return (v.Compare(o) == 1) +} + +// GTE checks if v is greater than or equal to o. +func (v Version) GTE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// GE checks if v is greater than or equal to o. +func (v Version) GE(o Version) bool { + return (v.Compare(o) >= 0) +} + +// LT checks if v is less than o. +func (v Version) LT(o Version) bool { + return (v.Compare(o) == -1) +} + +// LTE checks if v is less than or equal to o. +func (v Version) LTE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// LE checks if v is less than or equal to o. +func (v Version) LE(o Version) bool { + return (v.Compare(o) <= 0) +} + +// Compare compares Versions v to o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v Version) Compare(o Version) int { + if v.Major != o.Major { + if v.Major > o.Major { + return 1 + } + return -1 + } + if v.Minor != o.Minor { + if v.Minor > o.Minor { + return 1 + } + return -1 + } + if v.Patch != o.Patch { + if v.Patch > o.Patch { + return 1 + } + return -1 + } + + // Quick comparison if a version has no prerelease versions + if len(v.Pre) == 0 && len(o.Pre) == 0 { + return 0 + } else if len(v.Pre) == 0 && len(o.Pre) > 0 { + return 1 + } else if len(v.Pre) > 0 && len(o.Pre) == 0 { + return -1 + } + + i := 0 + for ; i < len(v.Pre) && i < len(o.Pre); i++ { + if comp := v.Pre[i].Compare(o.Pre[i]); comp == 0 { + continue + } else if comp == 1 { + return 1 + } else { + return -1 + } + } + + // If all pr versions are the equal but one has further prversion, this one greater + if i == len(v.Pre) && i == len(o.Pre) { + return 0 + } else if i == len(v.Pre) && i < len(o.Pre) { + return -1 + } else { + return 1 + } + +} + +// IncrementPatch increments the patch version +func (v *Version) IncrementPatch() error { + if v.Major == 0 { + return fmt.Errorf("Patch version can not be incremented for %q", v.String()) + } + v.Patch += 1 + return nil +} + +// IncrementMinor increments the minor version +func (v *Version) IncrementMinor() error { + if v.Major == 0 { + return fmt.Errorf("Minor version can not be incremented for %q", v.String()) + } + v.Minor += 1 + v.Patch = 0 + return nil +} + +// IncrementMajor increments the major version +func (v *Version) IncrementMajor() error { + if v.Major == 0 { + return fmt.Errorf("Major version can not be incremented for %q", v.String()) + } + v.Major += 1 + v.Minor = 0 + v.Patch = 0 + return nil +} + +// Validate validates v and returns error in case +func (v Version) Validate() error { + // Major, Minor, Patch already validated using uint64 + + for _, pre := range v.Pre { + if !pre.IsNum { //Numeric prerelease versions already uint64 + if len(pre.VersionStr) == 0 { + return fmt.Errorf("Prerelease can not be empty %q", pre.VersionStr) + } + if !containsOnly(pre.VersionStr, alphanum) { + return fmt.Errorf("Invalid character(s) found in prerelease %q", pre.VersionStr) + } + } + } + + for _, build := range v.Build { + if len(build) == 0 { + return fmt.Errorf("Build meta data can not be empty %q", build) + } + if !containsOnly(build, alphanum) { + return fmt.Errorf("Invalid character(s) found in build meta data %q", build) + } + } + + return nil +} + +// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error +func New(s string) (vp *Version, err error) { + v, err := Parse(s) + vp = &v + return +} + +// Make is an alias for Parse, parses version string and returns a validated Version or error +func Make(s string) (Version, error) { + return Parse(s) +} + +// ParseTolerant allows for certain version specifications that do not strictly adhere to semver +// specs to be parsed by this library. It does so by normalizing versions before passing them to +// Parse(). It currently trims spaces, removes a "v" prefix, adds a 0 patch number to versions +// with only major and minor components specified, and removes leading 0s. +func ParseTolerant(s string) (Version, error) { + s = strings.TrimSpace(s) + s = strings.TrimPrefix(s, "v") + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + // Remove leading zeros. + for i, p := range parts { + if len(p) > 1 { + parts[i] = strings.TrimPrefix(p, "0") + } + } + // Fill up shortened versions. + if len(parts) < 3 { + if strings.ContainsAny(parts[len(parts)-1], "+-") { + return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") + } + for len(parts) < 3 { + parts = append(parts, "0") + } + } + s = strings.Join(parts, ".") + + return Parse(s) +} + +// Parse parses version string and returns a validated Version or error +func Parse(s string) (Version, error) { + if len(s) == 0 { + return Version{}, errors.New("Version string empty") + } + + // Split into major.minor.(patch+pr+meta) + parts := strings.SplitN(s, ".", 3) + if len(parts) != 3 { + return Version{}, errors.New("No Major.Minor.Patch elements found") + } + + // Major + if !containsOnly(parts[0], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in major number %q", parts[0]) + } + if hasLeadingZeroes(parts[0]) { + return Version{}, fmt.Errorf("Major number must not contain leading zeroes %q", parts[0]) + } + major, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return Version{}, err + } + + // Minor + if !containsOnly(parts[1], numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in minor number %q", parts[1]) + } + if hasLeadingZeroes(parts[1]) { + return Version{}, fmt.Errorf("Minor number must not contain leading zeroes %q", parts[1]) + } + minor, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return Version{}, err + } + + v := Version{} + v.Major = major + v.Minor = minor + + var build, prerelease []string + patchStr := parts[2] + + if buildIndex := strings.IndexRune(patchStr, '+'); buildIndex != -1 { + build = strings.Split(patchStr[buildIndex+1:], ".") + patchStr = patchStr[:buildIndex] + } + + if preIndex := strings.IndexRune(patchStr, '-'); preIndex != -1 { + prerelease = strings.Split(patchStr[preIndex+1:], ".") + patchStr = patchStr[:preIndex] + } + + if !containsOnly(patchStr, numbers) { + return Version{}, fmt.Errorf("Invalid character(s) found in patch number %q", patchStr) + } + if hasLeadingZeroes(patchStr) { + return Version{}, fmt.Errorf("Patch number must not contain leading zeroes %q", patchStr) + } + patch, err := strconv.ParseUint(patchStr, 10, 64) + if err != nil { + return Version{}, err + } + + v.Patch = patch + + // Prerelease + for _, prstr := range prerelease { + parsedPR, err := NewPRVersion(prstr) + if err != nil { + return Version{}, err + } + v.Pre = append(v.Pre, parsedPR) + } + + // Build meta data + for _, str := range build { + if len(str) == 0 { + return Version{}, errors.New("Build meta data is empty") + } + if !containsOnly(str, alphanum) { + return Version{}, fmt.Errorf("Invalid character(s) found in build meta data %q", str) + } + v.Build = append(v.Build, str) + } + + return v, nil +} + +// MustParse is like Parse but panics if the version cannot be parsed. +func MustParse(s string) Version { + v, err := Parse(s) + if err != nil { + panic(`semver: Parse(` + s + `): ` + err.Error()) + } + return v +} + +// PRVersion represents a PreRelease Version +type PRVersion struct { + VersionStr string + VersionNum uint64 + IsNum bool +} + +// NewPRVersion creates a new valid prerelease version +func NewPRVersion(s string) (PRVersion, error) { + if len(s) == 0 { + return PRVersion{}, errors.New("Prerelease is empty") + } + v := PRVersion{} + if containsOnly(s, numbers) { + if hasLeadingZeroes(s) { + return PRVersion{}, fmt.Errorf("Numeric PreRelease version must not contain leading zeroes %q", s) + } + num, err := strconv.ParseUint(s, 10, 64) + + // Might never be hit, but just in case + if err != nil { + return PRVersion{}, err + } + v.VersionNum = num + v.IsNum = true + } else if containsOnly(s, alphanum) { + v.VersionStr = s + v.IsNum = false + } else { + return PRVersion{}, fmt.Errorf("Invalid character(s) found in prerelease %q", s) + } + return v, nil +} + +// IsNumeric checks if prerelease-version is numeric +func (v PRVersion) IsNumeric() bool { + return v.IsNum +} + +// Compare compares two PreRelease Versions v and o: +// -1 == v is less than o +// 0 == v is equal to o +// 1 == v is greater than o +func (v PRVersion) Compare(o PRVersion) int { + if v.IsNum && !o.IsNum { + return -1 + } else if !v.IsNum && o.IsNum { + return 1 + } else if v.IsNum && o.IsNum { + if v.VersionNum == o.VersionNum { + return 0 + } else if v.VersionNum > o.VersionNum { + return 1 + } else { + return -1 + } + } else { // both are Alphas + if v.VersionStr == o.VersionStr { + return 0 + } else if v.VersionStr > o.VersionStr { + return 1 + } else { + return -1 + } + } +} + +// PreRelease version to string +func (v PRVersion) String() string { + if v.IsNum { + return strconv.FormatUint(v.VersionNum, 10) + } + return v.VersionStr +} + +func containsOnly(s string, set string) bool { + return strings.IndexFunc(s, func(r rune) bool { + return !strings.ContainsRune(set, r) + }) == -1 +} + +func hasLeadingZeroes(s string) bool { + return len(s) > 1 && s[0] == '0' +} + +// NewBuildVersion creates a new valid build version +func NewBuildVersion(s string) (string, error) { + if len(s) == 0 { + return "", errors.New("Buildversion is empty") + } + if !containsOnly(s, alphanum) { + return "", fmt.Errorf("Invalid character(s) found in build meta data %q", s) + } + return s, nil +} diff --git a/vendor/github.com/blang/semver/sort.go b/vendor/github.com/blang/semver/sort.go new file mode 100644 index 000000000..e18f88082 --- /dev/null +++ b/vendor/github.com/blang/semver/sort.go @@ -0,0 +1,28 @@ +package semver + +import ( + "sort" +) + +// Versions represents multiple versions. +type Versions []Version + +// Len returns length of version collection +func (s Versions) Len() int { + return len(s) +} + +// Swap swaps two versions inside the collection by its indices +func (s Versions) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +// Less checks if version at index i is less than version at index j +func (s Versions) Less(i, j int) bool { + return s[i].LT(s[j]) +} + +// Sort sorts a slice of versions +func Sort(versions []Version) { + sort.Sort(Versions(versions)) +} diff --git a/vendor/github.com/blang/semver/sql.go b/vendor/github.com/blang/semver/sql.go new file mode 100644 index 000000000..db958134f --- /dev/null +++ b/vendor/github.com/blang/semver/sql.go @@ -0,0 +1,30 @@ +package semver + +import ( + "database/sql/driver" + "fmt" +) + +// Scan implements the database/sql.Scanner interface. +func (v *Version) Scan(src interface{}) (err error) { + var str string + switch src := src.(type) { + case string: + str = src + case []byte: + str = string(src) + default: + return fmt.Errorf("version.Scan: cannot convert %T to string", src) + } + + if t, err := Parse(str); err == nil { + *v = t + } + + return +} + +// Value implements the database/sql/driver.Valuer interface. +func (v Version) Value() (driver.Value, error) { + return v.String(), nil +} From 9eb8e3d67bcd373924ae097e1a85c004d4eae569 Mon Sep 17 00:00:00 2001 From: Peter Braun Date: Tue, 23 Apr 2019 09:43:36 +0200 Subject: [PATCH 5/5] init container image as param --- pkg/controller/grafana/templateHelper.go | 3 +++ templates/grafana-deployment.yaml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/controller/grafana/templateHelper.go b/pkg/controller/grafana/templateHelper.go index 974388b02..dda2fa096 100644 --- a/pkg/controller/grafana/templateHelper.go +++ b/pkg/controller/grafana/templateHelper.go @@ -22,6 +22,7 @@ const ( GrafanaDeploymentName = "grafana-deployment" GrafanaRouteName = "grafana-route" GrafanaServiceName = "grafana-service" + PluginsInitContainerImageTag = "0.0.1" ) type GrafanaParamaeters struct { @@ -38,6 +39,7 @@ type GrafanaParamaeters struct { LogLevel string GrafanaRouteName string GrafanaServiceName string + PluginsInitContainerImageTag string } type GrafanaTemplateHelper struct { @@ -63,6 +65,7 @@ func newTemplateHelper(cr *integreatly.Grafana) *GrafanaTemplateHelper { LogLevel: LogLevel, GrafanaRouteName: GrafanaRouteName, GrafanaServiceName: GrafanaServiceName, + PluginsInitContainerImageTag: PluginsInitContainerImageTag, } templatePath := os.Getenv("TEMPLATE_PATH") diff --git a/templates/grafana-deployment.yaml b/templates/grafana-deployment.yaml index b3d6603da..5932b3694 100644 --- a/templates/grafana-deployment.yaml +++ b/templates/grafana-deployment.yaml @@ -27,7 +27,7 @@ spec: - env: - name: GRAFANA_PLUGINS value: "" - image: 'docker.io/pb82/grafana_plugins_init:latest' + image: 'quay.io/integreatly/grafana_plugins_init:{{ .PluginsInitContainerImageTag }}' imagePullPolicy: IfNotPresent name: grafana-plugins-init resources: {}