diff --git a/api/v1alpha1/grafanaorganization_types.go b/api/v1alpha1/grafanaorganization_types.go index d5c3f690..e050c286 100644 --- a/api/v1alpha1/grafanaorganization_types.go +++ b/api/v1alpha1/grafanaorganization_types.go @@ -66,21 +66,23 @@ type GrafanaOrganizationStatus struct { // DataSources is a list of grafana data sources that are available to the Grafana organization. // +optional - DataSources []DataSources `json:"dataSources"` + DataSources []DataSource `json:"dataSources"` } // DataSource defines the name and id for data sources. -type DataSources struct { +type DataSource struct { + // ID is the unique id of the data source. + ID int64 `json:"ID"` + // Name is the name of the data source. Name string `json:"name"` - - // ID is the unique id of the data source. - ID int64 `json:"id"` } //+kubebuilder:object:root=true //+kubebuilder:resource:scope=Cluster //+kubebuilder:subresource:status +//+kubebuilder:printcolumn:JSONPath=".spec.displayName",name=DisplayName,type=string +//+kubebuilder:printcolumn:JSONPath=".status.orgID",name=OrgID,type=integer // GrafanaOrganization is the Schema describing a Grafana organization. Its lifecycle is managed by the observability-operator. type GrafanaOrganization struct { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b2981706..ca1a92cd 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -25,16 +25,16 @@ import ( ) // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *DataSources) DeepCopyInto(out *DataSources) { +func (in *DataSource) DeepCopyInto(out *DataSource) { *out = *in } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSources. -func (in *DataSources) DeepCopy() *DataSources { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataSource. +func (in *DataSource) DeepCopy() *DataSource { if in == nil { return nil } - out := new(DataSources) + out := new(DataSource) in.DeepCopyInto(out) return out } @@ -123,7 +123,7 @@ func (in *GrafanaOrganizationStatus) DeepCopyInto(out *GrafanaOrganizationStatus *out = *in if in.DataSources != nil { in, out := &in.DataSources, &out.DataSources - *out = make([]DataSources, len(*in)) + *out = make([]DataSource, len(*in)) copy(*out, *in) } } diff --git a/config/crd/observability.giantswarm.io_grafanaorganizations.yaml b/config/crd/observability.giantswarm.io_grafanaorganizations.yaml index fd9be0c0..08c7673e 100644 --- a/config/crd/observability.giantswarm.io_grafanaorganizations.yaml +++ b/config/crd/observability.giantswarm.io_grafanaorganizations.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.16.3 + controller-gen.kubebuilder.io/version: v0.16.4 name: grafanaorganizations.observability.giantswarm.io spec: group: observability.giantswarm.io @@ -14,7 +14,14 @@ spec: singular: grafanaorganization scope: Cluster versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.displayName + name: DisplayName + type: string + - jsonPath: .status.orgID + name: OrgID + type: integer + name: v1alpha1 schema: openAPIV3Schema: description: GrafanaOrganization is the Schema describing a Grafana organization. @@ -81,7 +88,7 @@ spec: items: description: DataSource defines the name and id for data sources. properties: - id: + ID: description: ID is the unique id of the data source. format: int64 type: integer @@ -89,7 +96,7 @@ spec: description: Name is the name of the data source. type: string required: - - id + - ID - name type: object type: array diff --git a/internal/controller/grafanaorganization_controller.go b/internal/controller/grafanaorganization_controller.go index 9a8b2276..daa92aa0 100644 --- a/internal/controller/grafanaorganization_controller.go +++ b/internal/controller/grafanaorganization_controller.go @@ -18,10 +18,10 @@ package controller import ( "context" - "fmt" + "slices" + "strings" grafanaAPI "github.com/grafana/grafana-openapi-client-go/client" - grafanaAPIModels "github.com/grafana/grafana-openapi-client-go/models" "github.com/pkg/errors" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -79,6 +79,39 @@ func (r *GrafanaOrganizationReconciler) Reconcile(ctx context.Context, req ctrl. return r.reconcileCreate(ctx, grafanaOrganization) } +// SetupWithManager sets up the controller with the Manager. +func (r *GrafanaOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.GrafanaOrganization{}). + // Watch for grafana pod's status changes + Watches( + &v1.Pod{}, + handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { + var logger = log.FromContext(ctx) + var organizations v1alpha1.GrafanaOrganizationList + + err := mgr.GetClient().List(ctx, &organizations) + if err != nil { + logger.Error(err, "failed to list grafana organization CRs") + return []reconcile.Request{} + } + + // Reconcile all grafana organizations when the grafana pod is recreated + requests := make([]reconcile.Request, 0, len(organizations.Items)) + for _, organization := range organizations.Items { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: organization.Name, + }, + }) + } + return requests + }), + builder.WithPredicates(predicates.GrafanaPodRecreatedPredicate{}), + ). + Complete(r) +} + // reconcileCreate creates the grafanaOrganization. // reconcileCreate ensures the Grafana organization described in grafanaOrganization CR is created in Grafana. // This function is also responsible for: @@ -106,51 +139,119 @@ func (r GrafanaOrganizationReconciler) reconcileCreate(ctx context.Context, graf return ctrl.Result{}, nil } - // Ensure the first organization is renamed. - _, err := r.GrafanaAPI.Orgs.UpdateOrg(1, &grafanaAPIModels.UpdateOrgForm{ - Name: grafana.SharedOrgName, - }) - if err != nil { - logger.Error(err, fmt.Sprintf("failed to rename Main Org. to %s", grafana.SharedOrgName)) + // Configure the shared organization in Grafana + if err := r.configureSharedOrg(ctx); err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + + // Configure the organization in Grafana + if err := r.configureOrganization(ctx, grafanaOrganization); err != nil { return ctrl.Result{}, errors.WithStack(err) } - // TODO add datasources for shared org. + // Update the datasources in the CR's status + if err := r.configureDatasources(ctx, grafanaOrganization); err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + + // Configure Grafana RBAC + if err := r.configureGrafana(ctx); err != nil { + return ctrl.Result{}, errors.WithStack(err) + } + + return ctrl.Result{}, nil +} + +func (r GrafanaOrganizationReconciler) configureSharedOrg(ctx context.Context) error { + logger := log.FromContext(ctx) + + sharedOrg := grafana.SharedOrg + + logger.Info("configuring shared organization") + if _, err := grafana.UpdateOrganization(ctx, r.GrafanaAPI, sharedOrg); err != nil { + logger.Error(err, "failed to rename shared org") + return errors.WithStack(err) + } + + if _, err := grafana.ConfigureDefaultDatasources(ctx, r.GrafanaAPI, sharedOrg); err != nil { + logger.Info("failed to configure datasources for shared org") + return errors.WithStack(err) + } + logger.Info("configured shared org") + return nil +} + +func (r GrafanaOrganizationReconciler) configureOrganization(ctx context.Context, grafanaOrganization *v1alpha1.GrafanaOrganization) (err error) { + logger := log.FromContext(ctx) // Create or update organization in Grafana - var organization = grafana.Organization{ - ID: grafanaOrganization.Status.OrgID, - Name: grafanaOrganization.Spec.DisplayName, + var organization = &grafana.Organization{ + ID: grafanaOrganization.Status.OrgID, + Name: grafanaOrganization.Spec.DisplayName, + TenantID: grafanaOrganization.Name, } if organization.ID == 0 { // if the CR doesn't have an orgID, create the organization in Grafana - organization, err = grafana.CreateOrganization(ctx, r.GrafanaAPI, organization) + organization, err = grafana.CreateOrganization(ctx, r.GrafanaAPI, *organization) } else { - organization, err = grafana.UpdateOrganization(ctx, r.GrafanaAPI, organization) + organization, err = grafana.UpdateOrganization(ctx, r.GrafanaAPI, *organization) } if err != nil { - return ctrl.Result{}, err + return errors.WithStack(err) } // Update CR status if anything was changed - if organization.ID != grafanaOrganization.Status.OrgID { + if grafanaOrganization.Status.OrgID != organization.ID { + logger.Info("updating orgID in the grafanaOrganization status") grafanaOrganization.Status.OrgID = organization.ID if err = r.Status().Update(ctx, grafanaOrganization); err != nil { logger.Error(err, "failed to update grafanaOrganization status") - return ctrl.Result{}, errors.WithStack(err) + return errors.WithStack(err) } + logger.Info("updated orgID in the grafanaOrganization status") } - // TODO add datasources for the organization. + return nil +} + +func (r GrafanaOrganizationReconciler) configureDatasources(ctx context.Context, grafanaOrganization *v1alpha1.GrafanaOrganization) error { + logger := log.FromContext(ctx) + + logger.Info("configuring data sources") - err = r.configureGrafana(ctx) + // Create or update organization in Grafana + var organization = grafana.Organization{ + ID: grafanaOrganization.Status.OrgID, + Name: grafanaOrganization.Spec.DisplayName, + TenantID: grafanaOrganization.Name, + } + + datasources, err := grafana.ConfigureDefaultDatasources(ctx, r.GrafanaAPI, organization) if err != nil { - return ctrl.Result{}, errors.WithStack(err) + return errors.WithStack(err) } - return ctrl.Result{}, nil + + var configuredDatasources = make([]v1alpha1.DataSource, len(datasources)) + for i, datasource := range datasources { + configuredDatasources[i] = v1alpha1.DataSource{ + ID: datasource.ID, + Name: datasource.Name, + } + } + + logger.Info("updating datasources in the grafanaOrganization status") + grafanaOrganization.Status.DataSources = configuredDatasources + if err := r.Status().Update(ctx, grafanaOrganization); err != nil { + logger.Error(err, "failed to update the the grafanaOrganization status with datasources information") + return errors.WithStack(err) + } + logger.Info("updated datasources in the grafanaOrganization status") + logger.Info("configured data sources") + + return nil } // reconcileDelete deletes the grafana organization. @@ -199,67 +300,41 @@ func (r GrafanaOrganizationReconciler) reconcileDelete(ctx context.Context, graf return nil } -// SetupWithManager sets up the controller with the Manager. -func (r *GrafanaOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&v1alpha1.GrafanaOrganization{}). - // Watch for grafana pod's status changes - Watches( - &v1.Pod{}, - handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, obj client.Object) []reconcile.Request { - k8sClient := mgr.GetClient() - var organizations v1alpha1.GrafanaOrganizationList - - err := k8sClient.List(ctx, &organizations) - if err != nil { - log.FromContext(ctx).Error(err, "failed to list grafana organization CRs") - return []reconcile.Request{} - } - - // Reconcile all grafana organizations when the grafana pod is recreated - requests := make([]reconcile.Request, 0, len(organizations.Items)) - for _, organization := range organizations.Items { - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Name: organization.Name, - }, - }) - } - return requests - }), - builder.WithPredicates(predicates.GrafanaPodRecreatedPredicate{}), - ). - Complete(r) -} - // configureGrafana ensures the RBAC configuration is set in Grafana. func (r *GrafanaOrganizationReconciler) configureGrafana(ctx context.Context) error { logger := log.FromContext(ctx) - organizations := v1alpha1.GrafanaOrganizationList{} - err := r.Client.List(ctx, &organizations) + organizationList := v1alpha1.GrafanaOrganizationList{} + err := r.Client.List(ctx, &organizationList) if err != nil { logger.Error(err, "failed to list grafana organizations.") return errors.WithStack(err) } - grafanaConfig := v1.ConfigMap{ + grafanaConfig := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "grafana-user-values", Namespace: "giantswarm", }, } - _, err = controllerutil.CreateOrPatch(ctx, r.Client, &grafanaConfig, func() error { - config, err := templating.GenerateGrafanaConfiguration(organizations.Items) + _, err = controllerutil.CreateOrPatch(ctx, r.Client, grafanaConfig, func() error { + organizations := organizationList.Items + // We always sort the organizations to ensure the order is deterministic and the configmap is stable + // in order to prevent grafana to restarts. + slices.SortFunc(organizations, func(i, j v1alpha1.GrafanaOrganization) int { + return strings.Compare(i.Name, j.Name) + }) + + config, err := templating.GenerateGrafanaConfiguration(organizations) if err != nil { logger.Error(err, "failed to generate grafana user configmap values.") return errors.WithStack(err) } - for _, organization := range organizations.Items { + for _, organization := range organizations { // Set owner reference to the config map to be able to clean it up when all organizations are deleted - err = controllerutil.SetOwnerReference(&organization, &grafanaConfig, r.Scheme) + err = controllerutil.SetOwnerReference(&organization, grafanaConfig, r.Scheme) if err != nil { return errors.WithStack(err) } diff --git a/internal/controller/predicates/predicates.go b/internal/controller/predicates/predicates.go index 85dedba9..fdda528d 100644 --- a/internal/controller/predicates/predicates.go +++ b/internal/controller/predicates/predicates.go @@ -3,19 +3,59 @@ package predicates import ( "strings" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" ) -// GrafanaPodRecreatedPredicate implements a default predicate function for grafana's pod deleted events. +// GrafanaPodRecreatedPredicate implements an eevent handler predicate function. +// This predicate is used to trigger a reconciliation when the Grafana pod is recreated to ensure the Grafana instance is up to date. type GrafanaPodRecreatedPredicate struct { predicate.Funcs } +func (GrafanaPodRecreatedPredicate) Create(e event.CreateEvent) bool { + // Do nothing as we want to act on Grafana pod creation event only. + return false +} + func (GrafanaPodRecreatedPredicate) Delete(e event.DeleteEvent) bool { - if e.Object != nil && strings.Contains(e.Object.GetName(), "grafana") && e.Object.GetNamespace() == "monitoring" { - return true + // Do nothing as we want to act on Grafana pod creation event only. + return false +} + +// When a grafana pod becomes ready, we want to trigger a reconciliation. +func (GrafanaPodRecreatedPredicate) Update(e event.UpdateEvent) bool { + if e.ObjectNew == nil { + return false + } + newPod, ok := e.ObjectNew.(*corev1.Pod) + if !ok { + return false } + return isGrafanaPod(newPod) && isPodReady(newPod) +} + +// isGrafanaPod checks if the object is a Grafana pod. +func isGrafanaPod(pod *corev1.Pod) bool { + return pod != nil && + strings.HasPrefix(pod.GetName(), "grafana") && + pod.GetNamespace() == "monitoring" && + pod.GetLabels() != nil && + pod.GetLabels()["app.kubernetes.io/instance"] == "grafana" +} + +// isPodReady checks if the pod is ready by inspecting its conditions. +func isPodReady(pod *corev1.Pod) bool { + for _, condition := range pod.Status.Conditions { + if condition.Type == corev1.PodReady && condition.Status == corev1.ConditionTrue { + return true + } + } + return false +} +func (GrafanaPodRecreatedPredicate) Generic(e event.GenericEvent) bool { + // Do nothing as we want to act on Grafana pod creation event only. return false } diff --git a/pkg/grafana/grafana.go b/pkg/grafana/grafana.go index c51229ed..08dc61e9 100644 --- a/pkg/grafana/grafana.go +++ b/pkg/grafana/grafana.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "fmt" + "strconv" "strings" "github.com/grafana/grafana-openapi-client-go/client" @@ -13,16 +14,57 @@ import ( ) const ( - SharedOrgName = "Shared Org" + datasourceProxyAccessMode = "proxy" ) -func CreateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) (Organization, error) { +var SharedOrg = Organization{ + ID: 1, + Name: "Shared Org", + TenantID: "giantswarm", +} + +// We need to use a custom name for now until we can replace the existing datasources. +var defaultDatasources = []Datasource{ + { + Name: "Alertmanager olly-op", + Type: "alertmanager", + IsDefault: true, + URL: "http://alertmanager-operated.monitoring.svc:9093", + Access: datasourceProxyAccessMode, + JSONData: map[string]interface{}{ + "handleGrafanaManagedAlerts": false, + "implementation": "prometheus", + }, + }, + { + Name: "Mimir olly-op", + Type: "prometheus", + IsDefault: true, + URL: "http://mimir-gateway.mimir.svc/prometheus", + Access: datasourceProxyAccessMode, + JSONData: map[string]interface{}{ + "cacheLevel": "None", + "httpMethod": "POST", + "mimirVersion": "2.14.0", + "prometheusType": "Mimir", + "timeInterval": "60s", + }, + }, + { + Name: "Loki olly-op", + Type: "loki", + URL: "http://loki-gateway.loki.svc", + Access: datasourceProxyAccessMode, + }, +} + +func CreateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) (*Organization, error) { logger := log.FromContext(ctx) logger.Info("creating organization") err := assertNameIsAvailable(ctx, grafanaAPI, organization) if err != nil { - return organization, errors.WithStack(err) + return nil, errors.WithStack(err) } createdOrg, err := grafanaAPI.Orgs.CreateOrg(&models.CreateOrgCommand{ @@ -30,17 +72,15 @@ func CreateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, }) if err != nil { logger.Error(err, "failed to create organization") - return organization, errors.WithStack(err) + return nil, errors.WithStack(err) } logger.Info("created organization") - return Organization{ - ID: *createdOrg.Payload.OrgID, - Name: organization.Name, - }, nil + organization.ID = *createdOrg.Payload.OrgID + return &organization, nil } -func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) (Organization, error) { +func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) (*Organization, error) { logger := log.FromContext(ctx) logger.Info("updating organization") @@ -52,18 +92,18 @@ func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, return CreateOrganization(ctx, grafanaAPI, organization) } logger.Error(err, fmt.Sprintf("failed to find organization with ID: %d", organization.ID)) - return organization, errors.WithStack(err) + return nil, errors.WithStack(err) } // If both name matches, there is nothing to do. if found.Name == organization.Name { logger.Info("the organization already exists in Grafana and does not need to be updated.") - return organization, nil + return &organization, nil } err = assertNameIsAvailable(ctx, grafanaAPI, organization) if err != nil { - return organization, errors.WithStack(err) + return nil, errors.WithStack(err) } // if the name of the CR is different from the name of the org in Grafana, update the name of the org in Grafana using the CR's display name. @@ -72,15 +112,12 @@ func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, }) if err != nil { logger.Error(err, "failed to update organization name") - return organization, errors.WithStack(err) + return nil, errors.WithStack(err) } logger.Info("updated organization") - return Organization{ - ID: organization.ID, - Name: organization.Name, - }, nil + return &organization, nil } func DeleteByID(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, id int64) error { @@ -102,6 +139,118 @@ func DeleteByID(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, id int64 return nil } +func ConfigureDefaultDatasources(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) ([]Datasource, error) { + logger := log.FromContext(ctx) + // TODO using a serviceaccount later would be better as they are scoped to an organization + + var err error + // Switch context to the current org + if _, err = grafanaAPI.SignedInUser.UserSetUsingOrg(organization.ID); err != nil { + logger.Error(err, "failed to change current org for signed in user") + return nil, errors.WithStack(err) + } + + // We always switch back to the shared org + defer func() { + if _, err = grafanaAPI.SignedInUser.UserSetUsingOrg(SharedOrg.ID); err != nil { + logger.Error(err, "failed to change current org for signed in user") + } + }() + + configuredDatasourcesInGrafana, err := listDatasourcesForOrganization(ctx, grafanaAPI) + if err != nil { + logger.Error(err, "failed to list datasources") + return nil, errors.WithStack(err) + } + + datasourcesToCreate := make([]Datasource, 0) + datasourcesToUpdate := make([]Datasource, 0) + + // Check if the default datasources are already configured + for _, defaultDatasource := range defaultDatasources { + found := false + for _, configuredDatasource := range configuredDatasourcesInGrafana { + if configuredDatasource.Name == defaultDatasource.Name { + found = true + + // We need to extract the ID from the configured datasource + datasourcesToUpdate = append(datasourcesToUpdate, defaultDatasource.withID(configuredDatasource.ID)) + break + } + } + if !found { + datasourcesToCreate = append(datasourcesToCreate, defaultDatasource) + } + } + + for index, datasource := range datasourcesToCreate { + logger.Info("creating datasource", "datasource", datasource.Name) + created, err := grafanaAPI.Datasources.AddDataSource( + &models.AddDataSourceCommand{ + Name: datasource.Name, + Type: datasource.Type, + URL: datasource.URL, + IsDefault: datasource.IsDefault, + JSONData: models.JSON(datasource.buildJSONData()), + SecureJSONData: datasource.buildSecureJSONData(organization), + Access: models.DsAccess(datasource.Access), + }) + if err != nil { + logger.Error(err, "failed to create datasources", "datasource", datasourcesToCreate[index].Name) + return nil, errors.WithStack(err) + } + datasourcesToCreate[index].ID = *created.Payload.ID + logger.Info("datasource created", "datasource", datasource.Name) + } + + for _, datasource := range datasourcesToUpdate { + logger.Info("updating datasource", "datasource", datasource.Name) + _, err := grafanaAPI.Datasources.UpdateDataSourceByID( + strconv.FormatInt(datasource.ID, 10), + &models.UpdateDataSourceCommand{ + Name: datasource.Name, + Type: datasource.Type, + URL: datasource.URL, + IsDefault: datasource.IsDefault, + JSONData: models.JSON(datasource.buildJSONData()), + SecureJSONData: datasource.buildSecureJSONData(organization), + Access: models.DsAccess(datasource.Access), + }) + if err != nil { + logger.Error(err, "failed to update datasources", "datasource", datasource.Name) + return nil, errors.WithStack(err) + } + logger.Info("datasource updated", "datasource", datasource.Name) + } + + // We return the datasources and the error if it exists. This allows us to return the defer function error it it exists. + return append(datasourcesToCreate, datasourcesToUpdate...), errors.WithStack(err) +} + +func listDatasourcesForOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI) ([]Datasource, error) { + logger := log.FromContext(ctx) + + resp, err := grafanaAPI.Datasources.GetDataSources() + if err != nil { + logger.Error(err, "failed to get configured datasources") + return nil, errors.WithStack(err) + } + + datasources := make([]Datasource, len(resp.Payload)) + for i, datasource := range resp.Payload { + datasources[i] = Datasource{ + ID: datasource.ID, + Name: datasource.Name, + IsDefault: datasource.IsDefault, + Type: datasource.Type, + URL: datasource.URL, + Access: string(datasource.Access), + } + } + + return datasources, nil +} + func isNotFound(err error) bool { if err == nil { return false diff --git a/pkg/grafana/templating/templates/grafana-user-values.yaml.template b/pkg/grafana/templating/templates/grafana-user-values.yaml.template index adfd272d..213e4e11 100644 --- a/pkg/grafana/templating/templates/grafana-user-values.yaml.template +++ b/pkg/grafana/templating/templates/grafana-user-values.yaml.template @@ -1,4 +1,8 @@ grafana: grafana.ini: + auth: + disable_signout_menu: false auth.generic_oauth: + role_attribute_path: to_string('Viewer') + org_attribute_path: groups org_mapping: '{{ .OrgMapping }}' diff --git a/pkg/grafana/templating/templating.go b/pkg/grafana/templating/templating.go index b7899740..33e64096 100644 --- a/pkg/grafana/templating/templating.go +++ b/pkg/grafana/templating/templating.go @@ -4,8 +4,8 @@ import ( "bytes" _ "embed" "fmt" - "html/template" "strings" + "text/template" "github.com/pkg/errors" @@ -31,7 +31,13 @@ func init() { func GenerateGrafanaConfiguration(organizations []v1alpha1.GrafanaOrganization) (string, error) { var orgMappings []string - orgMappings = append(orgMappings, fmt.Sprintf(`"*:%s:%s"`, grafana.SharedOrgName, grafanaAdminRole)) + // TODO: We need to be admins to be able to see the private dashboards for now, remove the 2 GS groups once https://github.com/giantswarm/roadmap/issues/3696 is done. + // Grant Admin role to Giantswarm users logging in via azure active directory. + orgMappings = append(orgMappings, buildOrgMapping(grafana.SharedOrg.Name, "giantswarm-ad:giantswarm-admins", grafanaAdminRole)) + // Grant Admin role to Giantswarm users logging in via github. + orgMappings = append(orgMappings, buildOrgMapping(grafana.SharedOrg.Name, "giantswarm-github:giantswarm:giantswarm-admins", grafanaAdminRole)) + // Grant Editor role to every other users. + orgMappings = append(orgMappings, fmt.Sprintf(`"*:%s:%s"`, grafana.SharedOrg.Name, grafanaEditorRole)) for _, organization := range organizations { rbac := organization.Spec.RBAC organizationName := organization.Spec.DisplayName diff --git a/pkg/grafana/types.go b/pkg/grafana/types.go index 503aedf7..a3242afe 100644 --- a/pkg/grafana/types.go +++ b/pkg/grafana/types.go @@ -1,6 +1,44 @@ package grafana type Organization struct { - ID int64 - Name string + ID int64 + Name string + TenantID string +} + +type Datasource struct { + ID int64 + Name string + IsDefault bool + Type string + URL string + Access string + JSONData map[string]interface{} +} + +func (d Datasource) withID(id int64) Datasource { + d.ID = id + return d +} + +func (d Datasource) buildJSONData() map[string]interface{} { + if d.JSONData == nil { + d.JSONData = make(map[string]interface{}) + } + + // Add tenant header name + d.JSONData["httpHeaderName1"] = "X-Scope-OrgID" + + return d.JSONData +} + +func (d Datasource) buildSecureJSONData(organization Organization) map[string]string { + tenant := organization.TenantID + if d.Name != "Loki" { + // We do not support multi-tenancy for Mimir yet + tenant = "anonymous" + } + return map[string]string{ + "httpHeaderValue1": tenant, + } }