Files
grafana/apps/iam/pkg/reconcilers/folder_reconciler.go
T
Alexander Zobnin e2d12f4cce Zanzana: Refactor remote client initialization (#114142)
* Zanzana: Refactor remote client

* rename config field URL to Addr

* Instrument grpc queries

* fix duplicated field
2025-12-11 10:55:12 +01:00

231 lines
6.8 KiB
Go

package reconcilers
import (
"context"
"fmt"
"time"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/operator"
foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/services/authz"
)
// PermissionStore interface for managing folder permissions
type PermissionStore interface {
GetFolderParents(ctx context.Context, namespace, folderUID string) ([]string, error)
SetFolderParent(ctx context.Context, namespace, folderUID, parentUID string) error
DeleteFolderParents(ctx context.Context, namespace, folderUID string) error
}
// ReconcilerConfig represents the app-specific configuration
type ReconcilerConfig struct {
ZanzanaCfg authz.ZanzanaClientConfig
Metrics *ReconcilerMetrics
}
type FolderReconciler struct {
permissionStore PermissionStore
metrics *ReconcilerMetrics
}
func NewFolderReconciler(cfg ReconcilerConfig, reg prometheus.Registerer) (operator.Reconciler, error) {
// Create Zanzana client
zanzanaClient, err := authz.NewRemoteZanzanaClient(cfg.ZanzanaCfg, reg)
if err != nil {
return nil, fmt.Errorf("unable to create zanzana client: %w", err)
}
// Create dependencies
permissionStore := NewZanzanaPermissionStore(zanzanaClient)
folderReconciler := &FolderReconciler{
permissionStore: permissionStore,
metrics: cfg.Metrics,
}
reconciler := &operator.TypedReconciler[*foldersKind.Folder]{
ReconcileFunc: folderReconciler.reconcile,
}
return reconciler, nil
}
// actionToString converts a ReconcileAction to a human-readable string
func actionToString(action operator.ReconcileAction) string {
switch action {
case operator.ReconcileActionCreated:
return "create"
case operator.ReconcileActionUpdated:
return "update"
case operator.ReconcileActionDeleted:
return "delete"
default:
return "unknown"
}
}
func (r *FolderReconciler) reconcile(ctx context.Context, req operator.TypedReconcileRequest[*foldersKind.Folder]) (operator.ReconcileResult, error) {
// Create root span for the entire reconciliation process
tracer := otel.GetTracerProvider().Tracer("iam-folder-reconciler")
ctx, span := tracer.Start(ctx, "folder.reconcile",
trace.WithAttributes(
attribute.String("action", actionToString(req.Action)),
attribute.String("folder.uid", req.Object.Name),
attribute.String("namespace", req.Object.Namespace),
),
)
defer span.End()
// Add timeout to prevent hanging operations
ctx, cancel := context.WithTimeout(ctx, 60*time.Second)
defer cancel()
action := actionToString(req.Action)
err := validateFolder(req.Object)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "validation failed")
if r.metrics != nil {
r.metrics.RecordReconcileFailure(action, "informer")
}
return operator.ReconcileResult{}, err
}
var result operator.ReconcileResult
switch req.Action {
case operator.ReconcileActionCreated:
result, err = r.handleUpdateFolder(ctx, req.Object, action)
case operator.ReconcileActionUpdated:
result, err = r.handleUpdateFolder(ctx, req.Object, action)
case operator.ReconcileActionDeleted:
result, err = r.handleDeleteFolder(ctx, req.Object, action)
default:
if r.metrics != nil {
r.metrics.RecordReconcileSuccess(action, "no_changes_needed")
}
return operator.ReconcileResult{}, nil
}
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "reconciliation failed")
}
return result, err
}
func (r *FolderReconciler) handleUpdateFolder(ctx context.Context, folder *foldersKind.Folder, action string) (operator.ReconcileResult, error) {
logger := logging.FromContext(ctx)
folderUID := folder.Name
namespace := folder.Namespace
parentUID, err := getFolderParent(ctx, folder)
if err != nil {
logger.Error("Error getting folder parent", "error", err)
if r.metrics != nil {
r.metrics.RecordReconcileFailure(action, "failure_informer")
}
return operator.ReconcileResult{}, err
}
parents, err := r.permissionStore.GetFolderParents(ctx, namespace, folderUID)
if err != nil {
logger.Error("Error getting folder parents", "error", err)
if r.metrics != nil {
r.metrics.RecordReconcileFailure(action, "permission_store")
}
return operator.ReconcileResult{}, err
}
if (len(parents) == 0 && parentUID == "") || (len(parents) == 1 && parents[0] == parentUID) {
logger.Info("Folder is already reconciled", "folder", folderUID, "parent", parentUID, "namespace", namespace)
if r.metrics != nil {
r.metrics.RecordReconcileSuccess(action, "no_changes_needed")
}
return operator.ReconcileResult{}, nil
}
err = r.permissionStore.SetFolderParent(ctx, namespace, folderUID, parentUID)
if err != nil {
logger.Error("Error setting folder parent", "error", err)
if r.metrics != nil {
r.metrics.RecordReconcileFailure(action, "permission_store")
}
return operator.ReconcileResult{}, err
}
logger.Info("Folder parent set in permission store", "folder", folderUID, "parent", parentUID, "namespace", namespace)
if r.metrics != nil {
r.metrics.RecordReconcileSuccess(action, "changes_made")
}
return operator.ReconcileResult{}, nil
}
func (r *FolderReconciler) handleDeleteFolder(ctx context.Context, folder *foldersKind.Folder, action string) (operator.ReconcileResult, error) {
logger := logging.FromContext(ctx)
namespace := folder.Namespace
folderUID := folder.Name
err := r.permissionStore.DeleteFolderParents(ctx, namespace, folderUID)
if err != nil {
logger.Error("Error deleting folder parents", "error", err)
if r.metrics != nil {
r.metrics.RecordReconcileFailure(action, "permission_store")
}
return operator.ReconcileResult{}, err
}
logger.Info("Folder deleted from permission store", "folder", folderUID, "namespace", namespace)
if r.metrics != nil {
r.metrics.RecordReconcileSuccess(action, "changes_made")
}
return operator.ReconcileResult{}, nil
}
func validateFolder(folder *foldersKind.Folder) error {
if folder == nil {
return fmt.Errorf("folder is nil")
}
if folder.Name == "" {
return fmt.Errorf("folder UID (ObjectMeta.Name) is empty")
}
if folder.Namespace == "" {
return fmt.Errorf("folder namespace is empty")
}
return nil
}
func getFolderParent(ctx context.Context, folder *foldersKind.Folder) (string, error) {
tracer := otel.GetTracerProvider().Tracer("iam-folder-reconciler")
_, span := tracer.Start(ctx, "get-folder-parent",
trace.WithAttributes(
attribute.String("folder.uid", folder.Name),
),
)
defer span.End()
folderMeta, err := utils.MetaAccessor(folder)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "failed to get folder meta accessor")
return "", err
}
return folderMeta.GetFolder(), nil
}