diff --git a/CHANGELOG.md b/CHANGELOG.md index 97724443..70bcb316 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,12 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add Mimir Alertmanager datasource +- Add Mimir Alertmanager datasource. +- Add tenant ids field to the grafana organization CR to be able to support multiple tenants into one organization. ### Changed - Removed organization OwnerReference on grafana-user-values configmap, this fixes an issue where the configmap is removed when the last organization is deleted which prevent Grafana from starting. +### Fixed + +- Fix grafana organization deletion + ## [0.9.1] - 2024-11-21 ### Fixed diff --git a/api/v1alpha1/grafanaorganization_types.go b/api/v1alpha1/grafanaorganization_types.go index e050c286..aebfe810 100644 --- a/api/v1alpha1/grafanaorganization_types.go +++ b/api/v1alpha1/grafanaorganization_types.go @@ -32,9 +32,19 @@ type GrafanaOrganizationSpec struct { DisplayName string `json:"displayName"` // Access rules defines user permissions for interacting with the organization in Grafana. - RBAC *RBAC `json:"rbac,omitempty"` + 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"` } +// 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: // @@ -62,6 +72,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. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index ca1a92cd..1cfc51aa 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -106,6 +106,11 @@ func (in *GrafanaOrganizationSpec) DeepCopyInto(out *GrafanaOrganizationSpec) { *out = new(RBAC) (*in).DeepCopyInto(*out) } + if in.Tenants != nil { + in, out := &in.Tenants, &out.Tenants + *out = make([]TenantID, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GrafanaOrganizationSpec. diff --git a/config/crd/observability.giantswarm.io_grafanaorganizations.yaml b/config/crd/observability.giantswarm.io_grafanaorganizations.yaml index 08c7673e..31927d97 100644 --- a/config/crd/observability.giantswarm.io_grafanaorganizations.yaml +++ b/config/crd/observability.giantswarm.io_grafanaorganizations.yaml @@ -76,8 +76,22 @@ 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 + type: array required: - displayName + - rbac type: object status: description: GrafanaOrganizationStatus defines the observed state of GrafanaOrganization @@ -104,8 +118,6 @@ spec: description: OrgID is the actual organisation ID in grafana. format: int64 type: integer - required: - - orgID type: object type: object served: true diff --git a/internal/controller/grafanaorganization_controller.go b/internal/controller/grafanaorganization_controller.go index c04c1d80..ca52bea4 100644 --- a/internal/controller/grafanaorganization_controller.go +++ b/internal/controller/grafanaorganization_controller.go @@ -305,9 +305,16 @@ func (r GrafanaOrganizationReconciler) reconcileDelete(ctx context.Context, graf return nil } + // Delete organization in Grafana + var organization = grafana.Organization{ + ID: grafanaOrganization.Status.OrgID, + Name: grafanaOrganization.Spec.DisplayName, + TenantID: grafanaOrganization.Name, + } + // Delete organization in Grafana if it exists if grafanaOrganization.Status.OrgID > 0 { - err := grafana.DeleteByID(ctx, r.GrafanaAPI, grafanaOrganization.Status.OrgID) + err := grafana.DeleteOrganization(ctx, r.GrafanaAPI, organization) if err != nil { return errors.WithStack(err) } diff --git a/pkg/grafana/grafana.go b/pkg/grafana/grafana.go index 5c3e0b30..3c7ffe11 100644 --- a/pkg/grafana/grafana.go +++ b/pkg/grafana/grafana.go @@ -131,16 +131,22 @@ func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, return &organization, nil } -func DeleteByID(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, id int64) error { +func DeleteOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) error { logger := log.FromContext(ctx) logger.Info("deleting organization") - _, err := findByID(grafanaAPI, id) + _, err := findByID(grafanaAPI, organization.ID) if err != nil { - logger.Error(err, fmt.Sprintf("failed to find organization with ID: %d", id)) + if isNotFound(err) { + logger.Info("organization id was not found, skipping deletion") + // If the CR orgID does not exist in Grafana, then we create the organization + return nil + } + logger.Error(err, fmt.Sprintf("failed to find organization with ID: %d", organization.ID)) + return errors.WithStack(err) } - _, err = grafanaAPI.Orgs.DeleteOrgByID(id) + _, err = grafanaAPI.Orgs.DeleteOrgByID(organization.ID) if err != nil { logger.Error(err, "failed to delete organization") return errors.WithStack(err)