mirror of https://github.com/grafana/grafana.git
Udate IAM Folder Reconciler Operator config (#110728)
This commit is contained in:
parent
e3e0a1b8ca
commit
62cc0f9c0e
|
@ -109,7 +109,7 @@ func LoadConfigFromEnv() (*Config, error) {
|
||||||
cfg.KubeConfig = kubeConfig
|
cfg.KubeConfig = kubeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.ZanzanaClient.Address = os.Getenv("ZANZANA_ADDR")
|
cfg.ZanzanaClient.URL = os.Getenv("ZANZANA_ADDR")
|
||||||
cfg.ZanzanaClient.Token = os.Getenv("ZANZANA_TOKEN")
|
cfg.ZanzanaClient.Token = os.Getenv("ZANZANA_TOKEN")
|
||||||
cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("TOKEN_EXCHANGE_URL")
|
cfg.ZanzanaClient.TokenExchangeURL = os.Getenv("TOKEN_EXCHANGE_URL")
|
||||||
cfg.ZanzanaClient.ServerCertFile = os.Getenv("ZANZANA_SERVER_CERT_FILE")
|
cfg.ZanzanaClient.ServerCertFile = os.Getenv("ZANZANA_SERVER_CERT_FILE")
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -98,7 +98,6 @@ 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,8 +1605,6 @@ 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=
|
||||||
|
|
|
@ -2,11 +2,13 @@ To build the operator, simply run `make build-go`
|
||||||
|
|
||||||
To run the folder reconciler, you need a `./conf/operator.ini` config file. For example:
|
To run the folder reconciler, you need a `./conf/operator.ini` config file. For example:
|
||||||
```
|
```
|
||||||
[iam_folder_reconciler]
|
[grpc_client_authentication]
|
||||||
folder_app_url = https://host.docker.internal:6446
|
token = IamFolderReconcilerToken
|
||||||
folder_app_namespace = *
|
|
||||||
zanzana_address = zanzana.default.svc.cluster.local:50051
|
|
||||||
token_exchange_url = http://host.docker.internal:8080/v1/sign-access-token
|
token_exchange_url = http://host.docker.internal:8080/v1/sign-access-token
|
||||||
token = ProvisioningAdminToken
|
|
||||||
|
[operator]
|
||||||
|
folder_app_url = https://host.docker.internal:6446
|
||||||
|
zanzana_url = zanzana.default.svc.cluster.local:50051
|
||||||
|
tls_insecure = true
|
||||||
```
|
```
|
||||||
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.
|
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.
|
||||||
|
|
|
@ -2,6 +2,7 @@ package iam
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
@ -10,9 +11,9 @@ import (
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/grafana/grafana-app-sdk/k8s"
|
|
||||||
"github.com/grafana/grafana-app-sdk/logging"
|
"github.com/grafana/grafana-app-sdk/logging"
|
||||||
"github.com/grafana/grafana-app-sdk/operator"
|
"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/apps/iam/pkg/app"
|
||||||
"github.com/grafana/grafana/pkg/server"
|
"github.com/grafana/grafana/pkg/server"
|
||||||
"github.com/grafana/grafana/pkg/services/apiserver/standalone"
|
"github.com/grafana/grafana/pkg/services/apiserver/standalone"
|
||||||
|
@ -22,7 +23,6 @@ import (
|
||||||
"k8s.io/client-go/transport"
|
"k8s.io/client-go/transport"
|
||||||
|
|
||||||
"github.com/grafana/authlib/authn"
|
"github.com/grafana/authlib/authn"
|
||||||
"github.com/grafana/grafana-app-sdk/plugin/kubeconfig"
|
|
||||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -78,64 +78,63 @@ type iamConfig struct {
|
||||||
AppConfig app.AppConfig
|
AppConfig app.AppConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
ConnTypeGRPC = "grpc"
|
|
||||||
ConnTypeHTTP = "http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildIAMConfigFromSettings(cfg *setting.Cfg) (*iamConfig, error) {
|
func buildIAMConfigFromSettings(cfg *setting.Cfg) (*iamConfig, error) {
|
||||||
var err error
|
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
return nil, fmt.Errorf("no configuration available")
|
return nil, fmt.Errorf("no configuration available")
|
||||||
}
|
}
|
||||||
|
|
||||||
iamCfg := iamConfig{}
|
iamCfg := iamConfig{}
|
||||||
|
|
||||||
iamFolderReconcilerSec := cfg.SectionWithEnvOverrides("iam_folder_reconciler")
|
gRPCAuth := cfg.SectionWithEnvOverrides("grpc_client_authentication")
|
||||||
|
token := gRPCAuth.Key("token").String()
|
||||||
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 == "" {
|
if token == "" {
|
||||||
return nil, fmt.Errorf("token is required in [iam_folder_reconciler] section")
|
return nil, fmt.Errorf("token is required in [grpc_client_authentication] section")
|
||||||
}
|
}
|
||||||
iamCfg.AppConfig.ZanzanaClientCfg.Token = token
|
iamCfg.AppConfig.ZanzanaClientCfg.Token = token
|
||||||
|
|
||||||
folderAppURL := iamFolderReconcilerSec.Key("folder_app_url").MustString("")
|
tokenExchangeURL := gRPCAuth.Key("token_exchange_url").String()
|
||||||
folderAppNamespace := iamFolderReconcilerSec.Key("folder_app_namespace").MustString("default")
|
if tokenExchangeURL == "" {
|
||||||
|
return nil, fmt.Errorf("token_exchange_url is required in [grpc_client_authentication] section")
|
||||||
|
}
|
||||||
|
iamCfg.AppConfig.ZanzanaClientCfg.TokenExchangeURL = tokenExchangeURL
|
||||||
|
|
||||||
kubeConfig, err := buildKubeConfigFromFolderAppURL(folderAppURL, tokenExchangeURL, token, folderAppNamespace)
|
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.URL = zanzanaURL
|
||||||
|
|
||||||
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to build kube config: %w", err)
|
return nil, fmt.Errorf("failed to build kube config: %w", err)
|
||||||
}
|
}
|
||||||
iamCfg.RunnerConfig.KubeConfig = kubeConfig.RestConfig
|
iamCfg.RunnerConfig.KubeConfig = *kubeConfig
|
||||||
|
|
||||||
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
|
return &iamCfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, namespace string) (*kubeconfig.NamespacedConfig, error) {
|
func buildKubeConfigFromFolderAppURL(
|
||||||
|
folderAppURL string,
|
||||||
|
exchangeUrl, authToken string,
|
||||||
|
tlsInsecure bool, tlsCertFile, tlsKeyFile, tlsCAFile string,
|
||||||
|
) (*rest.Config, error) {
|
||||||
tokenExchangeClient, err := authn.NewTokenExchangeClient(authn.TokenExchangeConfig{
|
tokenExchangeClient, err := authn.NewTokenExchangeClient(authn.TokenExchangeConfig{
|
||||||
TokenExchangeURL: exchangeUrl,
|
TokenExchangeURL: exchangeUrl,
|
||||||
Token: authToken,
|
Token: authToken,
|
||||||
|
@ -144,8 +143,12 @@ func buildKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, names
|
||||||
return nil, fmt.Errorf("failed to create token exchange client: %w", err)
|
return nil, fmt.Errorf("failed to create token exchange client: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &kubeconfig.NamespacedConfig{
|
tlsConfig, err := buildTLSConfig(tlsInsecure, tlsCertFile, tlsKeyFile, tlsCAFile)
|
||||||
RestConfig: rest.Config{
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build TLS configuration: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &rest.Config{
|
||||||
APIPath: "/apis",
|
APIPath: "/apis",
|
||||||
Host: folderAppURL,
|
Host: folderAppURL,
|
||||||
WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper {
|
WrapTransport: transport.WrapperFunc(func(rt http.RoundTripper) http.RoundTripper {
|
||||||
|
@ -154,14 +157,39 @@ func buildKubeConfigFromFolderAppURL(folderAppURL, exchangeUrl, authToken, names
|
||||||
transport: rt,
|
transport: rt,
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
TLSClientConfig: rest.TLSClientConfig{
|
TLSClientConfig: tlsConfig,
|
||||||
Insecure: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Namespace: namespace,
|
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
|
|
||||||
type authRoundTripper struct {
|
type authRoundTripper struct {
|
||||||
tokenExchangeClient *authn.TokenExchangeClient
|
tokenExchangeClient *authn.TokenExchangeClient
|
||||||
transport http.RoundTripper
|
transport http.RoundTripper
|
||||||
|
@ -169,7 +197,7 @@ type authRoundTripper struct {
|
||||||
|
|
||||||
func (t *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (t *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
tokenResponse, err := t.tokenExchangeClient.Exchange(req.Context(), authn.TokenExchangeRequest{
|
tokenResponse, err := t.tokenExchangeClient.Exchange(req.Context(), authn.TokenExchangeRequest{
|
||||||
Audiences: []string{"folder.grafana.app"},
|
Audiences: []string{folder.GROUP},
|
||||||
Namespace: "*",
|
Namespace: "*",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -178,7 +206,6 @@ func (t *authRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||||
|
|
||||||
// clone the request as RTs are not expected to mutate the passed request
|
// clone the request as RTs are not expected to mutate the passed request
|
||||||
req = utilnet.CloneRequest(req)
|
req = utilnet.CloneRequest(req)
|
||||||
|
|
||||||
req.Header.Set("X-Access-Token", "Bearer "+tokenResponse.Token)
|
req.Header.Set("X-Access-Token", "Bearer "+tokenResponse.Token)
|
||||||
return t.transport.RoundTrip(req)
|
return t.transport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features
|
||||||
return NewZanzanaClient(
|
return NewZanzanaClient(
|
||||||
fmt.Sprintf("stacks-%s", cfg.StackID),
|
fmt.Sprintf("stacks-%s", cfg.StackID),
|
||||||
ZanzanaClientConfig{
|
ZanzanaClientConfig{
|
||||||
Address: cfg.ZanzanaClient.Addr,
|
URL: cfg.ZanzanaClient.Addr,
|
||||||
Token: cfg.ZanzanaClient.Token,
|
Token: cfg.ZanzanaClient.Token,
|
||||||
TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL,
|
TokenExchangeURL: cfg.ZanzanaClient.TokenExchangeURL,
|
||||||
ServerCertFile: cfg.ZanzanaClient.ServerCertFile,
|
ServerCertFile: cfg.ZanzanaClient.ServerCertFile,
|
||||||
|
@ -94,7 +94,7 @@ func ProvideZanzana(cfg *setting.Cfg, db db.DB, tracer tracing.Tracer, features
|
||||||
}
|
}
|
||||||
|
|
||||||
type ZanzanaClientConfig struct {
|
type ZanzanaClientConfig struct {
|
||||||
Address string
|
URL string
|
||||||
Token string
|
Token string
|
||||||
TokenExchangeURL string
|
TokenExchangeURL string
|
||||||
ServerCertFile string
|
ServerCertFile string
|
||||||
|
@ -128,7 +128,7 @@ func NewZanzanaClient(namespace string, cfg ZanzanaClientConfig) (zanzana.Client
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := grpc.NewClient(cfg.Address, dialOptions...)
|
conn, err := grpc.NewClient(cfg.URL, dialOptions...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err)
|
return nil, fmt.Errorf("failed to create zanzana client to remote server: %w", err)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue