e2d12f4cce
* Zanzana: Refactor remote client * rename config field URL to Addr * Instrument grpc queries * fix duplicated field
231 lines
6.8 KiB
Go
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
|
|
}
|