Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ metadata:
categories: OpenShift Optional, Logging & Tracing
certified: "false"
containerImage: docker.io/grafana/loki-operator:0.8.0
createdAt: "2025-09-12T11:03:05Z"
createdAt: "2025-09-29T10:55:29Z"
description: The Community Loki Operator provides Kubernetes native deployment
and management of Loki and related logging components.
features.operators.openshift.io/disconnected: "true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ metadata:
categories: OpenShift Optional, Logging & Tracing
certified: "false"
containerImage: docker.io/grafana/loki-operator:0.8.0
createdAt: "2025-09-12T11:03:03Z"
createdAt: "2025-09-29T10:55:27Z"
description: The Community Loki Operator provides Kubernetes native deployment
and management of Loki and related logging components.
operators.operatorframework.io/builder: operator-sdk-unknown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ metadata:
categories: OpenShift Optional, Logging & Tracing
certified: "false"
containerImage: quay.io/openshift-logging/loki-operator:0.1.0
createdAt: "2025-09-12T11:03:07Z"
createdAt: "2025-09-29T10:55:30Z"
description: |
The Loki Operator for OCP provides a means for configuring and managing a Loki stack for cluster logging.
## Prerequisites and Requirements
Expand Down
9 changes: 5 additions & 4 deletions operator/cmd/loki-operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,22 @@ func main() {
os.Exit(1)
}

if ctrlCfg.Gates.ServiceMonitors && ctrlCfg.Gates.OpenShift.Enabled && ctrlCfg.Gates.OpenShift.Dashboards {
if ctrlCfg.Gates.OpenShift.Enabled {
var ns string
ns, err = operator.GetNamespace()
if err != nil {
logger.Error(err, "unable to read in operator namespace")
os.Exit(1)
}

if err = (&lokictrl.DashboardsReconciler{
if err = (&lokictrl.ClusterScopeReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Log: logger.WithName("controllers").WithName(lokictrl.ControllerNameLokiDashboards),
Log: logger.WithName("controllers").WithName(lokictrl.ControllerNameLokiClusterScope),
OperatorNs: ns,
Dashboards: ctrlCfg.Gates.ServiceMonitors && ctrlCfg.Gates.OpenShift.Dashboards,
}).SetupWithManager(mgr); err != nil {
logger.Error(err, "unable to create controller", "controller", lokictrl.ControllerNameLokiDashboards)
logger.Error(err, "unable to create controller", "controller", lokictrl.ControllerNameLokiClusterScope)
os.Exit(1)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
"github.com/grafana/loki/operator/internal/handlers"
)

const ControllerNameLokiDashboards = "loki-dashboards"
const ControllerNameLokiClusterScope = "loki-cluster-scope"

var createOrDeletesPred = builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool { return false },
Expand All @@ -27,18 +27,19 @@ var createOrDeletesPred = builder.WithPredicates(predicate.Funcs{
GenericFunc: func(e event.GenericEvent) bool { return false },
})

// DashboardsReconciler deploys and removes the cluster-global resources needed
// for the metrics dashboards depending on whether any LokiStacks exist.
type DashboardsReconciler struct {
// ClusterScopeReconciler deploys and removes the cluster-global resources needed
// for the metrics dashboards and RBAC resources depending on whether any LokiStacks exist.
type ClusterScopeReconciler struct {
client.Client
Scheme *runtime.Scheme
Log logr.Logger
OperatorNs string
Dashboards bool
}

// Reconcile creates all LokiStack dashboard ConfigMap and PrometheusRule objects on OpenShift clusters when
// the at least one LokiStack custom resource exists or removes all when none.
func (r *DashboardsReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
func (r *ClusterScopeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var stacks lokiv1.LokiStackList
if err := r.List(ctx, &stacks, client.MatchingLabelsSelector{Selector: labels.Everything()}); err != nil {
return ctrl.Result{}, kverrors.Wrap(err, "failed to list any lokistack instances")
Expand All @@ -47,24 +48,24 @@ func (r *DashboardsReconciler) Reconcile(ctx context.Context, req ctrl.Request)
if len(stacks.Items) == 0 {
// Removes all LokiStack dashboard resources on OpenShift clusters when
// the last LokiStack custom resource is deleted.
if err := handlers.DeleteDashboards(ctx, r.Client, r.OperatorNs); err != nil {
if err := handlers.DeleteClusterScopedResources(ctx, r.Client, r.OperatorNs); err != nil {
return ctrl.Result{}, kverrors.Wrap(err, "failed to delete dashboard resources")
}
return ctrl.Result{}, nil
}

// Creates all LokiStack dashboard resources on OpenShift clusters when
// the first LokiStack custom resource is created.
if err := handlers.CreateDashboards(ctx, r.Log, r.OperatorNs, r.Client, r.Scheme); err != nil {
if err := handlers.CreateClusterScopedResources(ctx, r.Log, r.Dashboards, r.OperatorNs, r.Client, r.Scheme, stacks.Items); err != nil {
return ctrl.Result{}, kverrors.Wrap(err, "failed to create dashboard resources", "req", req)
}
return ctrl.Result{}, nil
}

// SetupWithManager sets up the controller with the Manager to only call this controller on create/delete/generic events.
func (r *DashboardsReconciler) SetupWithManager(mgr manager.Manager) error {
func (r *ClusterScopeReconciler) SetupWithManager(mgr manager.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&lokiv1.LokiStack{}, createOrDeletesPred).
Named(ControllerNameLokiDashboards).
Named(ControllerNameLokiClusterScope).
Complete(r)
}
52 changes: 0 additions & 52 deletions operator/internal/handlers/dashboards_create.go

This file was deleted.

31 changes: 0 additions & 31 deletions operator/internal/handlers/dashboards_delete.go

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package handlers

import (
"context"
"fmt"

"github.com/ViaQ/logerr/v2/kverrors"
"github.com/go-logr/logr"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" //nolint:typecheck
ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

lokiv1 "github.com/grafana/loki/operator/api/loki/v1"
"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/manifests"
"github.com/grafana/loki/operator/internal/manifests/openshift"
)

// CreateClusterScopedResources handles the LokiStack cluster scoped create events.
func CreateClusterScopedResources(ctx context.Context, log logr.Logger, dashboards bool, operatorNs string, k k8s.Client, s *runtime.Scheme, stacks []lokiv1.LokiStack) error {
// This has to be done here as to not introduce a circular dependency.
rulerSubjects := make([]rbacv1.Subject, 0, len(stacks))
for _, stack := range stacks {
rulerSubjects = append(rulerSubjects, rbacv1.Subject{
Kind: "ServiceAccount",
Name: manifests.RulerName(stack.Name),
Namespace: stack.Namespace,
})
}
opts := openshift.NewOptionsClusterScope(operatorNs, manifests.ClusterScopeLabels(), rulerSubjects)

objs := openshift.BuildRBAC(opts)
if dashboards {
objs = append(objs, openshift.BuildDashboards(opts.OperatorNs)...)
}

var errCount int32
for _, obj := range objs {
desired := obj.DeepCopyObject().(client.Object)
mutateFn := manifests.MutateFuncFor(obj, desired, nil)

op, err := ctrl.CreateOrUpdate(ctx, k, obj, mutateFn)
if err != nil {
log.Error(err, "failed to configure resource")
errCount++
continue
}

msg := fmt.Sprintf("Resource has been %s", op)
switch op {
case ctrlutil.OperationResultNone:
log.V(1).Info(msg)
default:
log.Info(msg)
}
}

if errCount > 0 {
return kverrors.New("failed to configure lokistack cluster-scoped resources")
}

// Delete legacy RBAC resources
// This needs to live here and not in DeleteClusterScopedResources as we want to
// delete the legacy RBAC resources when LokiStack is reconciled and not on delete.
var legacyObjs []client.Object
for _, stack := range stacks {
// This name would clash with the new cluster-scoped resources. Skip it.
if stack.Name == "lokistack" {
continue
}
legacyObjs = append(legacyObjs, openshift.LegacyRBAC(manifests.GatewayName(stack.Name), manifests.RulerName(stack.Name))...)
}
for _, obj := range legacyObjs {
key := client.ObjectKeyFromObject(obj)
if err := k.Delete(ctx, obj, &client.DeleteOptions{}); err != nil {
if apierrors.IsNotFound(err) {
continue
}
return kverrors.Wrap(err, "failed to delete resource", "kind", obj.GetObjectKind(), "key", key)
}
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func TestCreateDashboards_ReturnsResourcesInManagedNamespaces(t *testing.T) {

k.StatusStub = func() client.StatusWriter { return sw }

err := CreateDashboards(context.TODO(), logger, "test", k, scheme)
err := CreateClusterScopedResources(context.Background(), logger, true, "test", k, scheme, []lokiv1.LokiStack{stack})
require.NoError(t, err)

// make sure create was called
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package handlers

import (
"context"

"github.com/ViaQ/logerr/v2/kverrors"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/grafana/loki/operator/internal/external/k8s"
"github.com/grafana/loki/operator/internal/manifests"
"github.com/grafana/loki/operator/internal/manifests/openshift"
)

// DeleteClusterScopedResources removes all cluster-scoped resources.
func DeleteClusterScopedResources(ctx context.Context, k k8s.Client, operatorNs string) error {
// Since we are deleting we don't need to worry about the subjects.
opts := openshift.NewOptionsClusterScope(operatorNs, manifests.ClusterScopeLabels(), []rbacv1.Subject{})

objs := openshift.BuildRBAC(opts)
objs = append(objs, openshift.BuildDashboards(opts.OperatorNs)...)

for _, obj := range objs {
if err := k.Delete(ctx, obj, &client.DeleteOptions{}); err != nil {
if apierrors.IsNotFound(err) {
continue
}
return kverrors.Wrap(err, "failed to delete dashboard", "kind", obj.GetObjectKind(), "key", client.ObjectKeyFromObject(obj))
}
}
return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,24 @@ import (
"github.com/grafana/loki/operator/internal/manifests/openshift"
)

func TestDeleteDashboards(t *testing.T) {
objs, err := openshift.BuildDashboards("operator-ns")
require.NoError(t, err)
func TestDeleteClusterScopedResources(t *testing.T) {
opts := openshift.NewOptionsClusterScope("operator-ns", nil, nil)
objs := openshift.BuildRBAC(opts)
objs = append(objs, openshift.BuildDashboards(opts.OperatorNs)...)

k := &k8sfakes.FakeClient{}

err = DeleteDashboards(context.TODO(), k, "operator-ns")
err := DeleteClusterScopedResources(context.Background(), k, "operator-ns")
require.NoError(t, err)
require.Equal(t, k.DeleteCallCount(), len(objs))
}

func TestDeleteDashboards_ReturnsNoError_WhenNotFound(t *testing.T) {
func TestDeleteClusterScopedResources_ReturnsNoError_WhenNotFound(t *testing.T) {
k := &k8sfakes.FakeClient{}
k.DeleteStub = func(context.Context, client.Object, ...client.DeleteOption) error {
return apierrors.NewNotFound(schema.GroupResource{}, "something wasn't found")
}

err := DeleteDashboards(context.TODO(), k, "operator-ns")
err := DeleteClusterScopedResources(context.Background(), k, "operator-ns")
require.NoError(t, err)
}
2 changes: 0 additions & 2 deletions operator/internal/manifests/gateway_tenants.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,6 @@ func configureGatewayObjsForMode(objs []client.Object, opts Options) []client.Ob
}
}

openShiftObjs := openshift.BuildGatewayTenantModeObjects(opts.OpenShiftOptions)
objs = append(objs, openShiftObjs...)
}

return objs
Expand Down
Loading
Loading