Skip to content

Commit

Permalink
support-multiple-tenants-in-one-org
Browse files Browse the repository at this point in the history
  • Loading branch information
QuentinBisson committed Dec 10, 2024
1 parent 8df42b3 commit 999bc3c
Show file tree
Hide file tree
Showing 7 changed files with 273 additions and 22 deletions.
13 changes: 13 additions & 0 deletions api/v1alpha1/grafanaorganization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,24 @@ const (
// GrafanaOrganizationSpec defines the desired state of GrafanaOrganization
type GrafanaOrganizationSpec struct {
// DisplayName is the name displayed when viewing the organization in Grafana. It can be different from the actual org's name.
// +kubebuilder:example="Giant Swarm"
// +kubebuilder:validation:MinLength=1
DisplayName string `json:"displayName"`

// Access rules defines user permissions for interacting with the organization in Grafana.
RBAC *RBAC `json:"rbac,omitempty"`

// Tenants is a list of tenants that are associated with the Grafana organization.
// +kubebuilder:example={"giantswarm"}
Tenants []TenantID `json:"tenants"`
}

// TenantID is a unique identifier for a tenant. It must be lowercase.
// +kubebuilder:validation:Pattern="^[a-z]*$"
// +kubebuilder:validation:MinLength=1
// +kubebuilder:validation:MaxLength=63
type TenantID string

// RBAC defines the RoleBasedAccessControl configuration for the Grafana organization.
// Each fields represents the mapping to a Grafana role:
//
Expand Down Expand Up @@ -62,6 +74,7 @@ type RBAC struct {
// GrafanaOrganizationStatus defines the observed state of GrafanaOrganization
type GrafanaOrganizationStatus struct {
// OrgID is the actual organisation ID in grafana.
// +optional
OrgID int64 `json:"orgID"`

// DataSources is a list of grafana data sources that are available to the Grafana organization.
Expand Down
5 changes: 5 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 18 additions & 2 deletions config/crd/observability.giantswarm.io_grafanaorganizations.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ spec:
displayName:
description: DisplayName is the name displayed when viewing the organization
in Grafana. It can be different from the actual org's name.
example: Giant Swarm
minLength: 1
type: string
rbac:
description: Access rules defines user permissions for interacting
Expand All @@ -76,8 +78,24 @@ spec:
required:
- admins
type: object
tenants:
description: Tenants is a list of tenants that are associated with
the Grafana organization.
example:
- giantswarm
items:
description: TenantID is a unique identifier for a tenant. It must
be lowercase.
maxLength: 63
minLength: 1
pattern: ^[a-z]*$
type: string
minItems: 1
type: array
uniqueItems: true
required:
- displayName
- tenants
type: object
status:
description: GrafanaOrganizationStatus defines the observed state of GrafanaOrganization
Expand All @@ -104,8 +122,6 @@ spec:
description: OrgID is the actual organisation ID in grafana.
format: int64
type: integer
required:
- orgID
type: object
type: object
served: true
Expand Down
27 changes: 16 additions & 11 deletions internal/controller/grafanaorganization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,23 @@ func (r GrafanaOrganizationReconciler) configureSharedOrg(ctx context.Context) e
return nil
}

func newOrganization(grafanaOrganization *v1alpha1.GrafanaOrganization) *grafana.Organization {
tenantIDs := make([]string, len(grafanaOrganization.Spec.Tenants))
for i, tenant := range grafanaOrganization.Spec.Tenants {
tenantIDs[i] = string(tenant)
}

return &grafana.Organization{
ID: grafanaOrganization.Status.OrgID,
Name: grafanaOrganization.Spec.DisplayName,
TenantIDs: tenantIDs,
}
}

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,
TenantID: grafanaOrganization.Name,
}
var organization = newOrganization(grafanaOrganization)

if organization.ID == 0 {
// if the CR doesn't have an orgID, create the organization in Grafana
Expand Down Expand Up @@ -265,13 +274,9 @@ func (r GrafanaOrganizationReconciler) configureDatasources(ctx context.Context,
logger.Info("configuring data sources")

// Create or update organization in Grafana
var organization = grafana.Organization{
ID: grafanaOrganization.Status.OrgID,
Name: grafanaOrganization.Spec.DisplayName,
TenantID: grafanaOrganization.Name,
}
var organization = newOrganization(grafanaOrganization)

datasources, err := grafana.ConfigureDefaultDatasources(ctx, r.GrafanaAPI, organization)
datasources, err := grafana.ConfigureDefaultDatasources(ctx, r.GrafanaAPI, *organization)
if err != nil {
return errors.WithStack(err)
}
Expand Down
210 changes: 210 additions & 0 deletions internal/controller/predicates/predicates_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package predicates

import (
"testing"

corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/event"
)

func TestIsGrafanaPod(t *testing.T) {
tests := []struct {
name string
pod *corev1.Pod
expected bool
}{
{
name: "nil pod",
pod: nil,
expected: false,
},
{
name: "non-Grafana pod",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "non-grafana-pod",
Namespace: "default",
},
},
expected: false,
},
{
name: "Grafana pod with correct labels",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "monitoring",
Labels: map[string]string{
"app.kubernetes.io/instance": "grafana",
},
},
},
expected: true,
},
{
name: "Grafana pod with incorrect namespace",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "default",
Labels: map[string]string{
"app.kubernetes.io/instance": "grafana",
},
},
},
expected: false,
},
{
name: "Grafana pod with incorrect label",
pod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "monitoring",
Labels: map[string]string{
"app.kubernetes.io/instance": "not-grafana",
},
},
},
expected: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := isGrafanaPod(tt.pod)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}

func TestGrafanaPodRecreatedPredicate_Update(t *testing.T) {
tests := []struct {
name string
oldPod *corev1.Pod
newPod *corev1.Pod
expected bool
}{
{
name: "nil old object",
oldPod: nil,
newPod: &corev1.Pod{},
expected: false,
},
{
name: "nil new object",
oldPod: &corev1.Pod{},
newPod: nil,
expected: false,
},
{
name: "non-Grafana pod",
oldPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "non-grafana-pod",
Namespace: "default",
},
},
newPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "non-grafana-pod",
Namespace: "default",
},
},
expected: false,
},
{
name: "Grafana pod not ready to ready",
oldPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "monitoring",
Labels: map[string]string{
"app.kubernetes.io/instance": "grafana",
},
},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionFalse,
},
},
},
},
newPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "monitoring",
Labels: map[string]string{
"app.kubernetes.io/instance": "grafana",
},
},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
},
},
},
},
expected: true,
},
{
name: "Grafana pod ready to not ready",
oldPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "monitoring",
Labels: map[string]string{
"app.kubernetes.io/instance": "grafana",
},
},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionTrue,
},
},
},
},
newPod: &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "grafana-pod",
Namespace: "monitoring",
Labels: map[string]string{
"app.kubernetes.io/instance": "grafana",
},
},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{
Type: corev1.PodReady,
Status: corev1.ConditionFalse,
},
},
},
},
expected: false,
},
}

predicate := GrafanaPodRecreatedPredicate{}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
e := event.UpdateEvent{
ObjectOld: tt.oldPod,
ObjectNew: tt.newPod,
}
result := predicate.Update(e)
if result != tt.expected {
t.Errorf("expected %v, got %v", tt.expected, result)
}
})
}
}
6 changes: 3 additions & 3 deletions pkg/grafana/grafana.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ const (
)

var SharedOrg = Organization{
ID: 1,
Name: "Shared Org",
TenantID: "giantswarm",
ID: 1,
Name: "Shared Org",
TenantIDs: []string{"giantswarm"},
}

// We need to use a custom name for now until we can replace the existing datasources.
Expand Down
14 changes: 8 additions & 6 deletions pkg/grafana/types.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package grafana

import "strings"

type Organization struct {
ID int64
Name string
TenantID string
ID int64
Name string
TenantIDs []string
}

type Datasource struct {
Expand Down Expand Up @@ -33,12 +35,12 @@ func (d Datasource) buildJSONData() map[string]interface{} {
}

func (d Datasource) buildSecureJSONData(organization Organization) map[string]string {
tenant := organization.TenantID
tenantIDs := organization.TenantIDs
if d.Type != "loki" {
// We do not support multi-tenancy for Mimir yet
tenant = "anonymous"
tenantIDs = []string{"anonymous"}
}
return map[string]string{
"httpHeaderValue1": tenant,
"httpHeaderValue1": strings.Join(tenantIDs, "|"),
}
}

0 comments on commit 999bc3c

Please sign in to comment.