Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

support-multiple-tenants-in-one-org #189

Merged
merged 4 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused, what have those test to do with the changes in this PR ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing, I forgot to push it in an earlier PR

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok happens, fine


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
Loading