Skip to content

Commit

Permalink
support-multiple-tenants-in-one-org (#189)
Browse files Browse the repository at this point in the history
* support-multiple-tenants-in-one-org

* remove useless pointer

* enforce tenants
  • Loading branch information
QuentinBisson authored Dec 10, 2024
1 parent f5bc38d commit 068fc03
Show file tree
Hide file tree
Showing 6 changed files with 259 additions and 40 deletions.
5 changes: 4 additions & 1 deletion api/v1alpha1/grafanaorganization_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,17 @@ 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"`

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

// TenantID is a unique identifier for a tenant. It must be lowercase.
Expand Down
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 Down Expand Up @@ -92,6 +94,7 @@ spec:
required:
- displayName
- rbac
- tenants
type: object
status:
description: GrafanaOrganizationStatus defines the observed state of GrafanaOrganization
Expand Down
38 changes: 19 additions & 19 deletions internal/controller/grafanaorganization_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ func (r GrafanaOrganizationReconciler) configureSharedOrg(ctx context.Context) e
sharedOrg := grafana.SharedOrg

logger.Info("configuring shared organization")
if _, err := grafana.UpdateOrganization(ctx, r.GrafanaAPI, sharedOrg); err != nil {
if err := grafana.UpdateOrganization(ctx, r.GrafanaAPI, &sharedOrg); err != nil {
logger.Error(err, "failed to rename shared org")
return errors.WithStack(err)
}
Expand All @@ -224,20 +224,28 @@ 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
organization, err = grafana.CreateOrganization(ctx, r.GrafanaAPI, *organization)
err = grafana.CreateOrganization(ctx, r.GrafanaAPI, &organization)
} else {
organization, err = grafana.UpdateOrganization(ctx, r.GrafanaAPI, *organization)
err = grafana.UpdateOrganization(ctx, r.GrafanaAPI, &organization)
}

if err != nil {
Expand Down Expand Up @@ -265,11 +273,7 @@ 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)
if err != nil {
Expand Down Expand Up @@ -306,11 +310,7 @@ func (r GrafanaOrganizationReconciler) reconcileDelete(ctx context.Context, graf
}

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

// Delete organization in Grafana if it exists
if grafanaOrganization.Status.OrgID > 0 {
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)
}
})
}
}
Loading

0 comments on commit 068fc03

Please sign in to comment.