From 7be0c100b65c8c6d24f614abec22396c822f9c75 Mon Sep 17 00:00:00 2001 From: Mihai Turdean <6640685+mihai-turdean@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:59:06 -0600 Subject: [PATCH] IAM Folder Reconciler - revert fetching latest folder info from the folder to improve perf (#111779) --- apps/iam/Tiltfile | 11 ++ apps/iam/cmd/operator/Dockerfile | 16 --- apps/iam/cmd/operator/authrt.go | 31 ----- apps/iam/cmd/operator/config.go | 131 ------------------ apps/iam/cmd/operator/kubeconfig.go | 85 ------------ apps/iam/cmd/operator/main.go | 87 ------------ apps/iam/go.mod | 5 +- apps/iam/go.sum | 2 - apps/iam/local/Dockerfile | 63 +++++++++ apps/iam/local/yamls/operator.yaml | 5 +- apps/iam/pkg/app/app.go | 1 - apps/iam/pkg/reconcilers/folder_reconciler.go | 33 +++-- apps/iam/pkg/reconcilers/folder_service.go | 76 ---------- apps/iam/pkg/reconcilers/metrics.go | 1 - 14 files changed, 100 insertions(+), 447 deletions(-) delete mode 100644 apps/iam/cmd/operator/Dockerfile delete mode 100644 apps/iam/cmd/operator/authrt.go delete mode 100644 apps/iam/cmd/operator/config.go delete mode 100644 apps/iam/cmd/operator/kubeconfig.go delete mode 100644 apps/iam/cmd/operator/main.go create mode 100644 apps/iam/local/Dockerfile delete mode 100644 apps/iam/pkg/reconcilers/folder_service.go diff --git a/apps/iam/Tiltfile b/apps/iam/Tiltfile index ab7c530e4b6..fa3c78faaf7 100644 --- a/apps/iam/Tiltfile +++ b/apps/iam/Tiltfile @@ -3,6 +3,17 @@ # https://docs.tilt.dev/api.html#api.version_settings version_settings(constraint='>=0.22.2') +custom_build( + 'iam-folder-reconciler', + command='docker buildx build --tag $EXPECTED_REF -f ./apps/iam/local/Dockerfile .', + deps=[ + 'apps/iam', + 'pkg', + ], + disable_push=True, + dir='../..', +) + k8s_yaml([filename for filename in listdir('local/yamls') if filename.lower().endswith(('.yaml', '.yml'))]) # Port forward Grafana to localhost:3000 diff --git a/apps/iam/cmd/operator/Dockerfile b/apps/iam/cmd/operator/Dockerfile deleted file mode 100644 index 513adce4f24..00000000000 --- a/apps/iam/cmd/operator/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM golang:1.24-alpine AS builder - -WORKDIR /build -COPY go.mod go.sum ./ -COPY vendor* ./vendor -RUN test -f vendor/modules.txt || go mod download - -COPY cmd cmd -COPY pkg pkg - -RUN go build -o "target/operator" cmd/operator/*.go - -FROM alpine AS runtime -COPY --from=builder /build/target/operator /usr/bin/operator - -ENTRYPOINT ["/usr/bin/operator"] \ No newline at end of file diff --git a/apps/iam/cmd/operator/authrt.go b/apps/iam/cmd/operator/authrt.go deleted file mode 100644 index 55d1c4cbecc..00000000000 --- a/apps/iam/cmd/operator/authrt.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - - utilnet "k8s.io/apimachinery/pkg/util/net" - - "github.com/grafana/authlib/authn" -) - -type authRoundTripper struct { - tokenExchangeClient *authn.TokenExchangeClient - transport http.RoundTripper -} - -func (t *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - tokenResponse, err := t.tokenExchangeClient.Exchange(req.Context(), authn.TokenExchangeRequest{ - Audiences: []string{"folder.grafana.app"}, - Namespace: "*", - }) - if err != nil { - return nil, fmt.Errorf("failed to exchange token: %w", err) - } - - // clone the request as RTs are not expected to mutate the passed request - req = utilnet.CloneRequest(req) - - req.Header.Set("X-Access-Token", "Bearer "+tokenResponse.Token) - return t.transport.RoundTrip(req) -} diff --git a/apps/iam/cmd/operator/config.go b/apps/iam/cmd/operator/config.go deleted file mode 100644 index f5481dfb9bd..00000000000 --- a/apps/iam/cmd/operator/config.go +++ /dev/null @@ -1,131 +0,0 @@ -package main - -import ( - "fmt" - "os" - "strconv" - "strings" - - "github.com/grafana/grafana-app-sdk/plugin/kubeconfig" - "github.com/grafana/grafana-app-sdk/simple" - "github.com/grafana/grafana/pkg/services/authz" -) - -const ( - ConnTypeGRPC = "grpc" - ConnTypeHTTP = "http" -) - -type Config struct { - OTelConfig simple.OpenTelemetryConfig - WebhookServer WebhookServerConfig - KubeConfig *kubeconfig.NamespacedConfig - ZanzanaClient authz.ZanzanaClientConfig - FolderReconciler FolderReconcilerConfig -} - -type WebhookServerConfig struct { - Port int - TLSCertPath string - TLSKeyPath string -} - -type FolderReconcilerConfig struct { - Namespace string - MaxConcurrentWorkers uint64 -} - -func LoadConfigFromEnv() (*Config, error) { - cfg := Config{} - cfg.OTelConfig.ServiceName = os.Getenv("OTEL_SERVICE_NAME") - switch strings.ToLower(os.Getenv("OTEL_CONN_TYPE")) { - case ConnTypeGRPC: - cfg.OTelConfig.ConnType = ConnTypeGRPC - case ConnTypeHTTP: - cfg.OTelConfig.ConnType = ConnTypeHTTP - case "": - // Default - cfg.OTelConfig.ConnType = ConnTypeHTTP - default: - return nil, fmt.Errorf("unknown OTEL_CONN_TYPE '%s'", os.Getenv("OTEL_CONN_TYPE")) - } - cfg.OTelConfig.Host = os.Getenv("OTEL_HOST") - portStr := os.Getenv("OTEL_PORT") - if portStr == "" { - if cfg.OTelConfig.ConnType == ConnTypeGRPC { - // Default OTel GRPC port - cfg.OTelConfig.Port = 4317 - } else { - // Default OTel HTTP port - cfg.OTelConfig.Port = 4318 - } - } else { - var err error - cfg.OTelConfig.Port, err = strconv.Atoi(portStr) - if err != nil { - return nil, fmt.Errorf("invalid OTEL_PORT '%s': %w", portStr, err) - } - } - - whPortStr := os.Getenv("WEBHOOK_PORT") - if whPortStr == "" { - cfg.WebhookServer.Port = 8443 - } else { - var err error - cfg.WebhookServer.Port, err = strconv.Atoi(whPortStr) - if err != nil { - return nil, fmt.Errorf("invalid WEBHOOK_PORT '%s': %w", whPortStr, err) - } - } - - cfg.WebhookServer.TLSCertPath = os.Getenv("WEBHOOK_CERT_PATH") - cfg.WebhookServer.TLSKeyPath = os.Getenv("WEBHOOK_KEY_PATH") - - // Load the kube config - kubeConfigFile := os.Getenv("KUBE_CONFIG_FILE") - if kubeConfigFile != "" { - kubeConfig, err := LoadKubeConfigFromFile(kubeConfigFile) - if err != nil { - return nil, fmt.Errorf("unable to load kubernetes configuration from file '%s': %w", kubeConfigFile, err) - } - cfg.KubeConfig = kubeConfig - } else if folderAppURL := os.Getenv("FOLDER_APP_URL"); folderAppURL != "" { - exchangeUrl := os.Getenv("TOKEN_EXCHANGE_URL") - authToken := os.Getenv("AUTH_TOKEN") - namespace := os.Getenv("FOLDER_APP_NAMESPACE") - if exchangeUrl == "" || authToken == "" { - return nil, fmt.Errorf("TOKEN_EXCHANGE_URL and AUTH_TOKEN must be set when FOLDER_APP_URL is set") - } - - kubeConfig, err := LoadKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, namespace) - if err != nil { - return nil, fmt.Errorf("unable to load kubernetes configuration from folder app URL '%s': %w", folderAppURL, err) - } - cfg.KubeConfig = kubeConfig - } else { - kubeConfig, err := LoadInClusterConfig() - if err != nil { - return nil, fmt.Errorf("unable to load in-cluster kubernetes configuration: %w", err) - } - cfg.KubeConfig = kubeConfig - } - - cfg.ZanzanaClient.URL = os.Getenv("ZANZANA_ADDR") - cfg.ZanzanaClient.Token = os.Getenv("ZANZANA_TOKEN") - cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("TOKEN_EXCHANGE_URL") - cfg.ZanzanaClient.ServerCertFile = os.Getenv("ZANZANA_SERVER_CERT_FILE") - - cfg.FolderReconciler.Namespace = os.Getenv("FOLDER_RECONCILER_NAMESPACE") - maxConcurrentWorkersStr := os.Getenv("FOLDER_RECONCILER_MAX_CONCURRENT_WORKERS") - if maxConcurrentWorkersStr == "" { - cfg.FolderReconciler.MaxConcurrentWorkers = 20 - } else { - maxConcurrentWorkers, err := strconv.ParseUint(maxConcurrentWorkersStr, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid FOLDER_RECONCILER_MAX_CONCURRENT_WORKERS '%s': %w", maxConcurrentWorkersStr, err) - } - cfg.FolderReconciler.MaxConcurrentWorkers = maxConcurrentWorkers - } - - return &cfg, nil -} diff --git a/apps/iam/cmd/operator/kubeconfig.go b/apps/iam/cmd/operator/kubeconfig.go deleted file mode 100644 index b704979cd9a..00000000000 --- a/apps/iam/cmd/operator/kubeconfig.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - - "k8s.io/client-go/rest" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/transport" - - "github.com/grafana/authlib/authn" - "github.com/grafana/grafana-app-sdk/plugin/kubeconfig" -) - -// LoadInClusterConfig loads a kubernetes in-cluster config. -// Since the in-cluster config doesn't have a namespace, it defaults to "default" -func LoadInClusterConfig() (*kubeconfig.NamespacedConfig, error) { - cfg, err := rest.InClusterConfig() - if err != nil { - return nil, err - } - cfg.APIPath = "/apis" - return &kubeconfig.NamespacedConfig{ - RestConfig: *cfg, - Namespace: "default", - }, nil -} - -// LoadKubeConfigFromEnv loads a NamespacedConfig from the value of an environment variable -func LoadKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, namespace string) (*kubeconfig.NamespacedConfig, error) { - tokenExchangeClient, err := authn.NewTokenExchangeClient(authn.TokenExchangeConfig{ - TokenExchangeURL: exchangeUrl, - Token: authToken, - }) - if err != nil { - return nil, fmt.Errorf("failed to create token exchange client: %w", err) - } - - return &kubeconfig.NamespacedConfig{ - RestConfig: rest.Config{ - APIPath: "/apis", - Host: folderAppURL, - WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper { - return &authRoundTripper{ - tokenExchangeClient: tokenExchangeClient, - transport: rt, - } - }), - TLSClientConfig: rest.TLSClientConfig{ - Insecure: true, - }, - }, - Namespace: namespace, - }, nil -} - -// LoadKubeConfigFromFile loads a NamespacedConfig from a file on-disk (such as a mounted secret) -func LoadKubeConfigFromFile(configPath string) (*kubeconfig.NamespacedConfig, error) { - // Load the kubeconfig file - config, err := clientcmd.LoadFromFile(configPath) - if err != nil { - return nil, fmt.Errorf("failed to load kubeconfig from %s: %w", configPath, err) - } - - // Build the REST config from the kubeconfig - restConfig, err := clientcmd.NewDefaultClientConfig(*config, &clientcmd.ConfigOverrides{}).ClientConfig() - if err != nil { - return nil, fmt.Errorf("failed to create REST config: %w", err) - } - - // Get the namespace from the current context, default to "default" if not set - namespace := "default" - if config.CurrentContext != "" { - if context, exists := config.Contexts[config.CurrentContext]; exists && context.Namespace != "" { - namespace = context.Namespace - } - } - - restConfig.APIPath = "/apis" - - return &kubeconfig.NamespacedConfig{ - RestConfig: *restConfig, - Namespace: namespace, - }, nil -} diff --git a/apps/iam/cmd/operator/main.go b/apps/iam/cmd/operator/main.go deleted file mode 100644 index 2824de11edd..00000000000 --- a/apps/iam/cmd/operator/main.go +++ /dev/null @@ -1,87 +0,0 @@ -package main - -import ( - "context" - "log/slog" - "os" - "os/signal" - - "github.com/grafana/grafana-app-sdk/k8s" - "github.com/grafana/grafana-app-sdk/logging" - "github.com/grafana/grafana-app-sdk/operator" - "github.com/grafana/grafana-app-sdk/simple" - "github.com/grafana/grafana/apps/iam/pkg/app" - "github.com/prometheus/client_golang/prometheus" -) - -func main() { - // Configure the default logger to use slog - logging.DefaultLogger = logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - Level: slog.LevelDebug, - })) - - //Load the config from the environment - cfg, err := LoadConfigFromEnv() - if err != nil { - logging.DefaultLogger.With("error", err).Error("Unable to load config from environment") - panic(err) - } - - // Set up tracing - if cfg.OTelConfig.Host != "" { - err = simple.SetTraceProvider(simple.OpenTelemetryConfig{ - Host: cfg.OTelConfig.Host, - Port: cfg.OTelConfig.Port, - ConnType: cfg.OTelConfig.ConnType, - ServiceName: cfg.OTelConfig.ServiceName, - }) - if err != nil { - logging.DefaultLogger.With("error", err).Error("Unable to set trace provider") - panic(err) - } - } - - // Create the operator config and the runner - operatorConfig := operator.RunnerConfig{ - KubeConfig: cfg.KubeConfig.RestConfig, - WebhookConfig: operator.RunnerWebhookConfig{ - Port: cfg.WebhookServer.Port, - TLSConfig: k8s.TLSConfig{ - CertPath: cfg.WebhookServer.TLSCertPath, - KeyPath: cfg.WebhookServer.TLSKeyPath, - }, - }, - MetricsConfig: operator.RunnerMetricsConfig{ - Enabled: true, - }, - } - - runner, err := operator.NewRunner(operatorConfig) - if err != nil { - logging.DefaultLogger.With("error", err).Error("Unable to create operator runner") - panic(err) - } - - // Context and cancel for the operator's Run method - ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) - defer cancel() - - // Create app config from operator config - appCfg := app.AppConfig{ - ZanzanaClientCfg: cfg.ZanzanaClient, - Namespace: cfg.FolderReconciler.Namespace, - InformerConfig: app.InformerConfig{ - MaxConcurrentWorkers: cfg.FolderReconciler.MaxConcurrentWorkers, - }, - MetricsRegisterer: prometheus.DefaultRegisterer, - } - - // Run - logging.DefaultLogger.Info("Starting operator") - err = runner.Run(ctx, app.Provider(appCfg)) - if err != nil { - logging.DefaultLogger.With("error", err).Error("Operator exited with error") - panic(err) - } - logging.DefaultLogger.Info("Normal operator exit") -} diff --git a/apps/iam/go.mod b/apps/iam/go.mod index a7ac326bd6c..1e77d44fd5d 100644 --- a/apps/iam/go.mod +++ b/apps/iam/go.mod @@ -21,18 +21,15 @@ replace github.com/grafana/grafana/pkg/aggregator => ../../pkg/aggregator replace github.com/prometheus/alertmanager => github.com/grafana/prometheus-alertmanager v0.25.1-0.20250911094103-5456b6e45604 require ( - github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c github.com/grafana/grafana v6.1.6+incompatible github.com/grafana/grafana-app-sdk v0.45.0 github.com/grafana/grafana-app-sdk/logging v0.45.0 - github.com/grafana/grafana-app-sdk/plugin v0.45.0 github.com/grafana/grafana/apps/folder v0.0.0 github.com/grafana/grafana/pkg/apimachinery v0.0.0 github.com/prometheus/client_golang v1.23.2 go.opentelemetry.io/otel v1.38.0 go.opentelemetry.io/otel/trace v1.38.0 k8s.io/apimachinery v0.34.1 - k8s.io/client-go v0.34.1 k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b ) @@ -205,6 +202,7 @@ require ( github.com/googleapis/gax-go/v2 v2.14.2 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/grafana/alerting v0.0.0-20250925200825-7a889aa4934d // indirect + github.com/grafana/authlib v0.0.0-20250924100039-ea07223cdb6c // indirect github.com/grafana/authlib/types v0.0.0-20250917093142-83a502239781 // indirect github.com/grafana/dataplane/sdata v0.0.9 // indirect github.com/grafana/dskit v0.0.0-20250908063411-6b6da59b5cc4 // indirect @@ -439,6 +437,7 @@ require ( k8s.io/api v0.34.1 // indirect k8s.io/apiextensions-apiserver v0.34.1 // indirect k8s.io/apiserver v0.34.1 // indirect + k8s.io/client-go v0.34.1 // indirect k8s.io/component-base v0.34.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kms v0.34.1 // indirect diff --git a/apps/iam/go.sum b/apps/iam/go.sum index b561836b7fd..ac09e1a53d7 100644 --- a/apps/iam/go.sum +++ b/apps/iam/go.sum @@ -737,8 +737,6 @@ github.com/grafana/grafana-app-sdk v0.45.0 h1:niFqYovxuw9vnUB9qoxEgmupqriG7Gns9Z github.com/grafana/grafana-app-sdk v0.45.0/go.mod h1:1pYGEBrgG8i6pKmmsNXvtAr15jZ4iLtyHU4yj7T6XaI= github.com/grafana/grafana-app-sdk/logging v0.45.0 h1:0SH6nYZpiLBZRwUq4J6+1vo8xuHKJjnO95/2pGOoA8w= github.com/grafana/grafana-app-sdk/logging v0.45.0/go.mod h1:Gh/nBWnspK3oDNWtiM5qUF/fardHzOIEez+SPI3JeHA= -github.com/grafana/grafana-app-sdk/plugin v0.45.0 h1:rK4FL5h7SqGBDeUdrUjHVGnwgN6w8deQAKx8gJ27Iew= -github.com/grafana/grafana-app-sdk/plugin v0.45.0/go.mod h1:fZ6lWVMWr0EpkmyocxZ7MTTc9x6b2jYM93+gjlMgVD4= github.com/grafana/grafana-aws-sdk v1.2.0 h1:LLR4/g91WBuCRwm2cbWfCREq565+GxIFe08nqqIcIuw= github.com/grafana/grafana-aws-sdk v1.2.0/go.mod h1:bBo7qOmM3f61vO+2JxTolNUph1l2TmtzmWcU9/Im+8A= github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA= diff --git a/apps/iam/local/Dockerfile b/apps/iam/local/Dockerfile new file mode 100644 index 00000000000..28df71c2384 --- /dev/null +++ b/apps/iam/local/Dockerfile @@ -0,0 +1,63 @@ +# Build stage +FROM golang:1.24.6-alpine AS builder + +# Set working directory +WORKDIR /app + +# Install bash (required for update-workspace.sh) and build tools (required for CGO) +RUN apk add --no-cache bash build-base + +# Copy source code +COPY ./apps ./apps +COPY ./pkg ./pkg +COPY ./conf ./conf +COPY ./go.mod ./go.mod +COPY ./go.sum ./go.sum +COPY ./go.work ./go.work +COPY ./go.work.sum ./go.work.sum +COPY ./build.go ./build.go +COPY ./package.json ./package.json + +# Update workspace +COPY scripts/go-workspace/update-workspace.sh ./scripts/go-workspace/update-workspace.sh +RUN bash ./scripts/go-workspace/update-workspace.sh + +# Build the application in dev mode to output binaries directly to ./bin/ +RUN go run build.go -dev build-backend + +# Final stage +FROM alpine:latest + +# Install ca-certificates for HTTPS requests and wget for health checks +RUN apk --no-cache add ca-certificates tzdata wget + +# Create non-root user +RUN addgroup -g 1001 -S appgroup && \ + adduser -u 1001 -S appuser -G appgroup + +# Set working directory +WORKDIR /usr/share/grafana + +# Copy all built binaries and conf from builder stage +COPY --from=builder /app/bin/ ./bin/ +COPY --from=builder /app/conf/ ./conf/ + +# Create necessary directories and add binaries to PATH +RUN mkdir -p /etc/grafana-config && \ + chown -R appuser:appgroup /usr/share/grafana /etc/grafana-config + +# Switch to non-root user +USER appuser + +# Add binaries to PATH +ENV PATH="/usr/share/grafana/bin:${PATH}" + +# Expose ports for metrics and profiling +EXPOSE 8080 6060 + +# Health check +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/metrics || exit 1 + +# Run the application with the command from the YAML (can be overridden) +CMD ["grafana", "server", "target", "--config=/etc/grafana-config/operator.ini", "--homepath=/usr/share/grafana"] diff --git a/apps/iam/local/yamls/operator.yaml b/apps/iam/local/yamls/operator.yaml index 574b9ad5b7d..6e17d5d670b 100644 --- a/apps/iam/local/yamls/operator.yaml +++ b/apps/iam/local/yamls/operator.yaml @@ -56,7 +56,8 @@ spec: spec: containers: - command: - - grafana-server + - grafana + - server - target - --config=/etc/grafana-config/operator.ini - --homepath=/usr/share/grafana @@ -78,7 +79,7 @@ spec: secretKeyRef: name: iam-operator-secrets key: grpc_auth_token - image: grafana/grafana-dev:12.3.0-17863745596 + image: iam-folder-reconciler imagePullPolicy: IfNotPresent name: iam-folder-reconciler volumeMounts: diff --git a/apps/iam/pkg/app/app.go b/apps/iam/pkg/app/app.go index ddaeb86b8dd..c84128e76b4 100644 --- a/apps/iam/pkg/app/app.go +++ b/apps/iam/pkg/app/app.go @@ -79,7 +79,6 @@ func New(cfg app.Config) (app.App, error) { folderReconciler, err := reconcilers.NewFolderReconciler(reconcilers.ReconcilerConfig{ ZanzanaCfg: appSpecificConfig.ZanzanaClientCfg, - KubeConfig: &cfg.KubeConfig, Metrics: metrics, }) if err != nil { diff --git a/apps/iam/pkg/reconcilers/folder_reconciler.go b/apps/iam/pkg/reconcilers/folder_reconciler.go index f06b4b2e60c..59385b3b3a3 100644 --- a/apps/iam/pkg/reconcilers/folder_reconciler.go +++ b/apps/iam/pkg/reconcilers/folder_reconciler.go @@ -8,19 +8,14 @@ import ( "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" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" - "k8s.io/client-go/rest" ) -// FolderStore interface for retrieving folder information -type FolderStore interface { - GetFolderParent(ctx context.Context, namespace, uid string) (string, error) -} - // PermissionStore interface for managing folder permissions type PermissionStore interface { GetFolderParents(ctx context.Context, namespace, folderUID string) ([]string, error) @@ -31,13 +26,11 @@ type PermissionStore interface { // ReconcilerConfig represents the app-specific configuration type ReconcilerConfig struct { ZanzanaCfg authz.ZanzanaClientConfig - KubeConfig *rest.Config Metrics *ReconcilerMetrics } type FolderReconciler struct { permissionStore PermissionStore - folderStore FolderStore metrics *ReconcilerMetrics } @@ -50,12 +43,10 @@ func NewFolderReconciler(cfg ReconcilerConfig) (operator.Reconciler, error) { } // Create dependencies - folderStore := NewAPIFolderStore(cfg.KubeConfig) permissionStore := NewZanzanaPermissionStore(zanzanaClient) folderReconciler := &FolderReconciler{ permissionStore: permissionStore, - folderStore: folderStore, metrics: cfg.Metrics, } @@ -137,11 +128,11 @@ func (r *FolderReconciler) handleUpdateFolder(ctx context.Context, folder *folde folderUID := folder.Name namespace := folder.Namespace - parentUID, err := r.folderStore.GetFolderParent(ctx, namespace, folderUID) + parentUID, err := getFolderParent(ctx, folder) if err != nil { logger.Error("Error getting folder parent", "error", err) if r.metrics != nil { - r.metrics.RecordReconcileFailure(action, "folder_store") + r.metrics.RecordReconcileFailure(action, "failure_informer") } return operator.ReconcileResult{}, err } @@ -217,3 +208,21 @@ func validateFolder(folder *foldersKind.Folder) error { } 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 +} diff --git a/apps/iam/pkg/reconcilers/folder_service.go b/apps/iam/pkg/reconcilers/folder_service.go deleted file mode 100644 index 0a1a74a4257..00000000000 --- a/apps/iam/pkg/reconcilers/folder_service.go +++ /dev/null @@ -1,76 +0,0 @@ -package reconcilers - -import ( - "context" - "fmt" - - foldersKind "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1" - "github.com/grafana/grafana/pkg/apimachinery/utils" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/dynamic" - "k8s.io/client-go/rest" -) - -var _ FolderStore = (*APIFolderStore)(nil) - -func NewAPIFolderStore(config *rest.Config) FolderStore { - return &APIFolderStore{config} -} - -type APIFolderStore struct { - config *rest.Config -} - -func (s *APIFolderStore) GetFolderParent(ctx context.Context, namespace, uid string) (string, error) { - tracer := otel.GetTracerProvider().Tracer("iam-folder-reconciler") - ctx, span := tracer.Start(ctx, "APIFolderStore.GetFolderParent", - trace.WithAttributes( - attribute.String("folder.uid", uid), - attribute.String("folder.namespace", namespace), - ), - ) - defer span.End() - - client, err := s.client(namespace) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to create kubernetes client") - return "", fmt.Errorf("create resource client: %w", err) - } - - // Get the folder by UID - unstructuredObj, err := client.Get(ctx, uid, metav1.GetOptions{}) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to get folder from kubernetes API") - return "", fmt.Errorf("get folder %s: %w", uid, err) - } - - object, err := utils.MetaAccessor(unstructuredObj) - if err != nil { - span.RecordError(err) - span.SetStatus(codes.Error, "failed to get meta accessor from folder object") - return "", fmt.Errorf("get meta accessor: %w", err) - } - - parentUID := object.GetFolder() - span.SetAttributes(attribute.String("folder.parent_uid", parentUID)) - span.SetStatus(codes.Ok, "successfully retrieved folder parent") - span.AddEvent("folder.parent.retrieved", trace.WithAttributes( - attribute.String("parent.uid", parentUID), - )) - - return parentUID, nil -} - -func (s *APIFolderStore) client(namespace string) (dynamic.ResourceInterface, error) { - client, err := dynamic.NewForConfig(s.config) - if err != nil { - return nil, err - } - return client.Resource(foldersKind.FolderResourceInfo.GroupVersionResource()).Namespace(namespace), nil -} diff --git a/apps/iam/pkg/reconcilers/metrics.go b/apps/iam/pkg/reconcilers/metrics.go index 65a14e32df5..10802744105 100644 --- a/apps/iam/pkg/reconcilers/metrics.go +++ b/apps/iam/pkg/reconcilers/metrics.go @@ -22,7 +22,6 @@ func NewReconcilerMetrics(registerer prometheus.Registerer, namespace string) *R "success_no_changes_needed", "failure_informer", "failure_permission_store", - "failure_folder_store", "failure_unknown", }, }