IAM Folder Reconciler - revert fetching latest folder info from the folder to improve perf (#111779)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
+2
-3
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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"]
|
||||
@@ -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:
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user