mirror of
https://github.com/grafana/grafana.git
synced 2025-09-17 21:53:15 +08:00
IAM Folder Reconciler: Make operator available as a target when running the grafana binary. (#110567)
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -139,6 +139,7 @@
|
|||||||
/pkg/apimachinery @grafana/grafana-app-platform-squad
|
/pkg/apimachinery @grafana/grafana-app-platform-squad
|
||||||
/pkg/apimachinery/identity/ @grafana/identity-squad
|
/pkg/apimachinery/identity/ @grafana/identity-squad
|
||||||
/pkg/apimachinery/errutil/ @grafana/grafana-backend-group
|
/pkg/apimachinery/errutil/ @grafana/grafana-backend-group
|
||||||
|
/pkg/operators/iam @grafana/access-squad
|
||||||
/pkg/promlib @grafana/oss-big-tent
|
/pkg/promlib @grafana/oss-big-tent
|
||||||
/pkg/storage/ @grafana/grafana-search-and-storage
|
/pkg/storage/ @grafana/grafana-search-and-storage
|
||||||
/pkg/storage/secret/ @grafana/grafana-operator-experience-squad
|
/pkg/storage/secret/ @grafana/grafana-operator-experience-squad
|
||||||
|
@ -67,7 +67,7 @@ func main() {
|
|||||||
|
|
||||||
// Create app config from operator config
|
// Create app config from operator config
|
||||||
appCfg := app.AppConfig{
|
appCfg := app.AppConfig{
|
||||||
ZanzanaCfg: cfg.ZanzanaClient,
|
ZanzanaClientCfg: cfg.ZanzanaClient,
|
||||||
FolderReconcilerNamespace: cfg.FolderReconciler.Namespace,
|
FolderReconcilerNamespace: cfg.FolderReconciler.Namespace,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,7 +18,7 @@ var appManifestData = app.ManifestData{
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
ZanzanaCfg authz.ZanzanaClientConfig
|
ZanzanaClientCfg authz.ZanzanaClientConfig
|
||||||
FolderReconcilerNamespace string
|
FolderReconcilerNamespace string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ func New(cfg app.Config) (app.App, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
folderReconciler, err := reconcilers.NewFolderReconciler(reconcilers.ReconcilerConfig{
|
folderReconciler, err := reconcilers.NewFolderReconciler(reconcilers.ReconcilerConfig{
|
||||||
ZanzanaCfg: appSpecificConfig.ZanzanaCfg,
|
ZanzanaCfg: appSpecificConfig.ZanzanaClientCfg,
|
||||||
KubeConfig: &cfg.KubeConfig,
|
KubeConfig: &cfg.KubeConfig,
|
||||||
FolderReconcilerNamespace: appSpecificConfig.FolderReconcilerNamespace,
|
FolderReconcilerNamespace: appSpecificConfig.FolderReconcilerNamespace,
|
||||||
})
|
})
|
||||||
|
1
go.mod
1
go.mod
@ -98,6 +98,7 @@ require (
|
|||||||
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
|
github.com/grafana/grafana-api-golang-client v0.27.0 // @grafana/alerting-backend
|
||||||
github.com/grafana/grafana-app-sdk v0.40.3 // @grafana/grafana-app-platform-squad
|
github.com/grafana/grafana-app-sdk v0.40.3 // @grafana/grafana-app-platform-squad
|
||||||
github.com/grafana/grafana-app-sdk/logging v0.40.3 // @grafana/grafana-app-platform-squad
|
github.com/grafana/grafana-app-sdk/logging v0.40.3 // @grafana/grafana-app-platform-squad
|
||||||
|
github.com/grafana/grafana-app-sdk/plugin v0.40.3 // @grafana/grafana-app-platform-squad
|
||||||
github.com/grafana/grafana-aws-sdk v1.1.0 // @grafana/aws-datasources
|
github.com/grafana/grafana-aws-sdk v1.1.0 // @grafana/aws-datasources
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 // @grafana/partner-datasources
|
||||||
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
|
github.com/grafana/grafana-cloud-migration-snapshot v1.9.0 // @grafana/grafana-operator-experience-squad
|
||||||
|
2
go.sum
2
go.sum
@ -1605,6 +1605,8 @@ github.com/grafana/grafana-app-sdk v0.40.3 h1:JFo7uAfbAJUfZ9neD7/4sODKm1xgu9zhck
|
|||||||
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
|
github.com/grafana/grafana-app-sdk v0.40.3/go.mod h1:j0KzHo3Sa6kd+lnwSScBNoV9Vobkg/YY9HtEjxpyPrk=
|
||||||
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
|
github.com/grafana/grafana-app-sdk/logging v0.40.3 h1:2VXsXXEQiqAavRP8wusRDB6rDqf5lufP7A6NfjELqPE=
|
||||||
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
github.com/grafana/grafana-app-sdk/logging v0.40.3/go.mod h1:otUD9XpJD7A5sCLb8mcs9hIXGdeV6lnhzVwe747g4RU=
|
||||||
|
github.com/grafana/grafana-app-sdk/plugin v0.40.3 h1:uH0oFZnYOUL+OXcyhd5NVYwoM+Wa0WUXvZ2Om1M91r0=
|
||||||
|
github.com/grafana/grafana-app-sdk/plugin v0.40.3/go.mod h1:+ylwE0P8WgPu5zURK5aDnVJpwRpuK3573rwrVV28qzQ=
|
||||||
github.com/grafana/grafana-aws-sdk v1.1.0 h1:G0fvwbQmHw14c5RXPd7Gnw9ZQcgzl139LtMDoe0KhmE=
|
github.com/grafana/grafana-aws-sdk v1.1.0 h1:G0fvwbQmHw14c5RXPd7Gnw9ZQcgzl139LtMDoe0KhmE=
|
||||||
github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8=
|
github.com/grafana/grafana-aws-sdk v1.1.0/go.mod h1:7e+47EdHynteYWGoT5Ere9KeOXQObsk8F0vkOLQ1tz8=
|
||||||
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA=
|
github.com/grafana/grafana-azure-sdk-go/v2 v2.2.0 h1:0TYrkzAc3u0HX+9GK86cGrLTUAcmQfl3/LEB3tL+SOA=
|
||||||
|
12
pkg/operators/iam/README.md
Normal file
12
pkg/operators/iam/README.md
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
To build the operator, simply run `make build-go`
|
||||||
|
|
||||||
|
To run the folder reconciler, you need a `./conf/operator.ini` config file. For example:
|
||||||
|
```
|
||||||
|
[iam_folder_reconciler]
|
||||||
|
folder_app_url = https://host.docker.internal:6446
|
||||||
|
folder_app_namespace = *
|
||||||
|
zanzana_address = zanzana.default.svc.cluster.local:50051
|
||||||
|
token_exchange_url = http://host.docker.internal:8080/v1/sign-access-token
|
||||||
|
token = ProvisioningAdminToken
|
||||||
|
```
|
||||||
|
After that, you can run it using: `GF_DEFAULT_TARGET=operator GF_OPERATOR_NAME=iam-folder-reconciler ./bin/linux-arm64/grafana server target --config=conf/operator.ini`. Beware that you will also need a TokenExchanger, a Zanzana Server and a Folder app running for the operator to behave.
|
184
pkg/operators/iam/zanzana_folder_reconciler.go
Normal file
184
pkg/operators/iam/zanzana_folder_reconciler.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package iam
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"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/apps/iam/pkg/app"
|
||||||
|
"github.com/grafana/grafana/pkg/server"
|
||||||
|
"github.com/grafana/grafana/pkg/services/apiserver/standalone"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"k8s.io/client-go/rest"
|
||||||
|
"k8s.io/client-go/transport"
|
||||||
|
|
||||||
|
"github.com/grafana/authlib/authn"
|
||||||
|
"github.com/grafana/grafana-app-sdk/plugin/kubeconfig"
|
||||||
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
server.RegisterOperator(server.Operator{
|
||||||
|
Name: "iam-folder-reconciler",
|
||||||
|
Description: "Watch folder resources and manage IAM permissions with Zanzana",
|
||||||
|
RunFunc: RunIAMFolderReconciler,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunIAMFolderReconciler(opts standalone.BuildInfo, c *cli.Context, cfg *setting.Cfg) 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(cfg)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ConnTypeGRPC = "grpc"
|
||||||
|
ConnTypeHTTP = "http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildIAMConfigFromSettings(cfg *setting.Cfg) (*iamConfig, error) {
|
||||||
|
var err error
|
||||||
|
if cfg == nil {
|
||||||
|
return nil, fmt.Errorf("no configuration available")
|
||||||
|
}
|
||||||
|
|
||||||
|
iamCfg := iamConfig{}
|
||||||
|
|
||||||
|
iamFolderReconcilerSec := cfg.SectionWithEnvOverrides("iam_folder_reconciler")
|
||||||
|
|
||||||
|
zanzanaAddress := iamFolderReconcilerSec.Key("zanzana_address").MustString("")
|
||||||
|
if zanzanaAddress == "" {
|
||||||
|
return nil, fmt.Errorf("address is required in [iam_folder_reconciler.zanzana] section")
|
||||||
|
}
|
||||||
|
iamCfg.AppConfig.ZanzanaClientCfg.Address = zanzanaAddress
|
||||||
|
|
||||||
|
tokenExchangeURL := iamFolderReconcilerSec.Key("token_exchange_url").MustString("")
|
||||||
|
if tokenExchangeURL == "" {
|
||||||
|
return nil, fmt.Errorf("token_exchange_url is required in [iam_folder_reconciler] section")
|
||||||
|
}
|
||||||
|
iamCfg.AppConfig.ZanzanaClientCfg.TokenExchangeURL = tokenExchangeURL
|
||||||
|
|
||||||
|
token := iamFolderReconcilerSec.Key("token").MustString("")
|
||||||
|
if token == "" {
|
||||||
|
return nil, fmt.Errorf("token is required in [iam_folder_reconciler] section")
|
||||||
|
}
|
||||||
|
iamCfg.AppConfig.ZanzanaClientCfg.Token = token
|
||||||
|
|
||||||
|
folderAppURL := iamFolderReconcilerSec.Key("folder_app_url").MustString("")
|
||||||
|
folderAppNamespace := iamFolderReconcilerSec.Key("folder_app_namespace").MustString("default")
|
||||||
|
|
||||||
|
kubeConfig, err := buildKubeConfigFromFolderAppURL(folderAppURL, tokenExchangeURL, token, folderAppNamespace)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build kube config: %w", err)
|
||||||
|
}
|
||||||
|
iamCfg.RunnerConfig.KubeConfig = kubeConfig.RestConfig
|
||||||
|
|
||||||
|
wenhookSection := cfg.SectionWithEnvOverrides("iam_folder_reconciler.webhook_server")
|
||||||
|
webhookPort := wenhookSection.Key("port").MustInt(8443)
|
||||||
|
webhookCertPath := wenhookSection.Key("cert_path").MustString("")
|
||||||
|
webhookKeyPath := wenhookSection.Key("key_path").MustString("")
|
||||||
|
iamCfg.RunnerConfig.WebhookConfig = operator.RunnerWebhookConfig{
|
||||||
|
Port: webhookPort,
|
||||||
|
TLSConfig: k8s.TLSConfig{
|
||||||
|
CertPath: webhookCertPath,
|
||||||
|
KeyPath: webhookKeyPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return &iamCfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildKubeConfigFromFolderAppURL(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
|
||||||
|
}
|
||||||
|
|
||||||
|
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,6 +1,7 @@
|
|||||||
package operators
|
package operators
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/operators/iam"
|
||||||
"github.com/grafana/grafana/pkg/operators/provisioning"
|
"github.com/grafana/grafana/pkg/operators/provisioning"
|
||||||
"github.com/grafana/grafana/pkg/server"
|
"github.com/grafana/grafana/pkg/server"
|
||||||
)
|
)
|
||||||
@ -17,4 +18,10 @@ func init() {
|
|||||||
Description: "Watch provisioning repositories",
|
Description: "Watch provisioning repositories",
|
||||||
RunFunc: provisioning.RunRepoController,
|
RunFunc: provisioning.RunRepoController,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
server.RegisterOperator(server.Operator{
|
||||||
|
Name: "iam-folder-reconciler",
|
||||||
|
Description: "Reconcile folder resources into Zanzana",
|
||||||
|
RunFunc: iam.RunIAMFolderReconciler,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user