* Chore: Unify token exchange rount trippers * Remove the conditional provider for now * Remove unecessary strategy * test cleanup * Lint
189 lines
5.6 KiB
Go
189 lines
5.6 KiB
Go
package iam
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"k8s.io/client-go/rest"
|
|
|
|
"github.com/grafana/grafana-app-sdk/logging"
|
|
"github.com/grafana/grafana-app-sdk/operator"
|
|
folder "github.com/grafana/grafana/apps/folder/pkg/apis/folder/v1beta1"
|
|
"github.com/grafana/grafana/apps/iam/pkg/app"
|
|
"github.com/grafana/grafana/pkg/clientauth"
|
|
"github.com/grafana/grafana/pkg/server"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
|
|
"github.com/grafana/authlib/authn"
|
|
)
|
|
|
|
func RunIAMFolderReconciler(deps server.OperatorDependencies) error {
|
|
logger := logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
|
Level: slog.LevelDebug,
|
|
})).With("logger", "iam-folder-reconciler")
|
|
logger.Info("Starting IAM folder reconciler operator")
|
|
|
|
// Get configuration from Grafana settings
|
|
iamConfig, err := buildIAMConfigFromSettings(deps.Config, deps.Registerer)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to build IAM config: %w", err)
|
|
}
|
|
|
|
runner, err := operator.NewRunner(iamConfig.RunnerConfig)
|
|
if err != nil {
|
|
logger.Error("failed to create IAM operator runner", "error", err)
|
|
return fmt.Errorf("failed to create IAM operator runner: %w", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
go func() {
|
|
<-sigChan
|
|
fmt.Println("Received shutdown signal, stopping IAM operator")
|
|
cancel()
|
|
}()
|
|
|
|
logger.Info("Starting IAM folder reconciler")
|
|
err = runner.Run(ctx, app.Provider(iamConfig.AppConfig))
|
|
if err != nil && !errors.Is(err, context.Canceled) {
|
|
return fmt.Errorf("IAM operator exited with error: %w", err)
|
|
}
|
|
|
|
logger.Info("IAM folder reconciler stopped")
|
|
return nil
|
|
}
|
|
|
|
type iamConfig struct {
|
|
RunnerConfig operator.RunnerConfig
|
|
AppConfig app.AppConfig
|
|
}
|
|
|
|
func buildIAMConfigFromSettings(cfg *setting.Cfg, registerer prometheus.Registerer) (*iamConfig, error) {
|
|
if cfg == nil {
|
|
return nil, fmt.Errorf("no configuration available")
|
|
}
|
|
|
|
iamCfg := iamConfig{}
|
|
|
|
gRPCAuth := cfg.SectionWithEnvOverrides("grpc_client_authentication")
|
|
token := gRPCAuth.Key("token").String()
|
|
if token == "" {
|
|
return nil, fmt.Errorf("token is required in [grpc_client_authentication] section")
|
|
}
|
|
iamCfg.AppConfig.ZanzanaClientCfg.Token = token
|
|
|
|
tokenExchangeURL := gRPCAuth.Key("token_exchange_url").String()
|
|
if tokenExchangeURL == "" {
|
|
return nil, fmt.Errorf("token_exchange_url is required in [grpc_client_authentication] section")
|
|
}
|
|
iamCfg.AppConfig.ZanzanaClientCfg.TokenExchangeURL = tokenExchangeURL
|
|
|
|
operatorSec := cfg.SectionWithEnvOverrides("operator")
|
|
|
|
zanzanaURL := operatorSec.Key("zanzana_url").MustString("")
|
|
if zanzanaURL == "" {
|
|
return nil, fmt.Errorf("zanzana_url is required in [operator] section")
|
|
}
|
|
iamCfg.AppConfig.ZanzanaClientCfg.Addr = zanzanaURL
|
|
|
|
iamCfg.AppConfig.InformerConfig.MaxConcurrentWorkers = operatorSec.Key("max_concurrent_workers").MustUint64(20)
|
|
|
|
folderAppURL := operatorSec.Key("folder_app_url").MustString("")
|
|
if folderAppURL == "" {
|
|
return nil, fmt.Errorf("folder_app_url is required in [operator] section")
|
|
}
|
|
|
|
tlsInsecure := operatorSec.Key("tls_insecure").MustBool(false)
|
|
tlsCertFile := operatorSec.Key("tls_cert_file").String()
|
|
tlsKeyFile := operatorSec.Key("tls_key_file").String()
|
|
tlsCAFile := operatorSec.Key("tls_ca_file").String()
|
|
iamCfg.AppConfig.ZanzanaClientCfg.ServerCertFile = tlsCertFile
|
|
|
|
kubeConfig, err := buildKubeConfigFromFolderAppURL(
|
|
folderAppURL,
|
|
tokenExchangeURL, token,
|
|
tlsInsecure, tlsCertFile, tlsKeyFile, tlsCAFile,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build kube config: %w", err)
|
|
}
|
|
iamCfg.RunnerConfig.KubeConfig = *kubeConfig
|
|
|
|
metricsSection := cfg.SectionWithEnvOverrides("metrics")
|
|
iamCfg.RunnerConfig.MetricsConfig.Enabled = metricsSection.Key("enabled").MustBool(true)
|
|
iamCfg.RunnerConfig.MetricsConfig.Namespace = metricsSection.Key("namespace").MustString("grafana-iam")
|
|
iamCfg.AppConfig.Namespace = iamCfg.RunnerConfig.MetricsConfig.Namespace
|
|
iamCfg.AppConfig.MetricsRegisterer = registerer
|
|
|
|
return &iamCfg, nil
|
|
}
|
|
|
|
func buildKubeConfigFromFolderAppURL(
|
|
folderAppURL string,
|
|
exchangeUrl, authToken string,
|
|
tlsInsecure bool, tlsCertFile, tlsKeyFile, tlsCAFile string,
|
|
) (*rest.Config, 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)
|
|
}
|
|
|
|
tlsConfig, err := buildTLSConfig(tlsInsecure, tlsCertFile, tlsKeyFile, tlsCAFile)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to build TLS configuration: %w", err)
|
|
}
|
|
|
|
return &rest.Config{
|
|
APIPath: "/apis",
|
|
Host: folderAppURL,
|
|
WrapTransport: clientauth.NewStaticTokenExchangeTransportWrapper(
|
|
tokenExchangeClient,
|
|
folder.GROUP,
|
|
clientauth.WildcardNamespace,
|
|
),
|
|
TLSClientConfig: tlsConfig,
|
|
}, nil
|
|
}
|
|
|
|
func buildTLSConfig(insecure bool, certFile, keyFile, caFile string) (rest.TLSClientConfig, error) {
|
|
tlsConfig := rest.TLSClientConfig{
|
|
Insecure: insecure,
|
|
}
|
|
|
|
if certFile != "" && keyFile != "" {
|
|
tlsConfig.CertFile = certFile
|
|
tlsConfig.KeyFile = keyFile
|
|
}
|
|
|
|
if caFile != "" {
|
|
// caFile is set in operator.ini file
|
|
// nolint:gosec
|
|
caCert, err := os.ReadFile(caFile)
|
|
if err != nil {
|
|
return tlsConfig, fmt.Errorf("failed to read CA certificate file: %w", err)
|
|
}
|
|
|
|
caCertPool := x509.NewCertPool()
|
|
if !caCertPool.AppendCertsFromPEM(caCert) {
|
|
return tlsConfig, fmt.Errorf("failed to parse CA certificate")
|
|
}
|
|
|
|
tlsConfig.CAData = caCert
|
|
}
|
|
|
|
return tlsConfig, nil
|
|
}
|