Files
2025-06-20 16:37:17 -05:00

535 lines
17 KiB
Go

package apiserver
import (
"context"
"fmt"
"net/http"
"path"
"github.com/prometheus/client_golang/prometheus"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
genericapifilters "k8s.io/apiserver/pkg/endpoints/filters"
"k8s.io/apiserver/pkg/endpoints/responsewriter"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/apiserver/pkg/util/notfoundhandler"
clientrest "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"github.com/grafana/dskit/services"
"github.com/grafana/grafana-plugin-sdk-go/backend"
dataplaneaggregator "github.com/grafana/grafana/pkg/aggregator/apiserver"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
grafanaresponsewriter "github.com/grafana/grafana/pkg/apiserver/endpoints/responsewriter"
"github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/kvstore"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/serverlock"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/middleware"
"github.com/grafana/grafana/pkg/modules"
servicetracing "github.com/grafana/grafana/pkg/modules/tracing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/registry/apis/datasource"
"github.com/grafana/grafana/pkg/services/apiserver/aggregatorrunner"
"github.com/grafana/grafana/pkg/services/apiserver/auth/authenticator"
"github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
"github.com/grafana/grafana/pkg/services/apiserver/utils"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"github.com/grafana/grafana/pkg/storage/unified/apistore"
"github.com/grafana/grafana/pkg/storage/unified/resource"
)
var (
_ Service = (*service)(nil)
_ RestConfigProvider = (*service)(nil)
_ registry.BackgroundService = (*service)(nil)
_ registry.CanBeDisabled = (*service)(nil)
)
const MaxRequestBodyBytes = 16 * 1024 * 1024 // 16MB - determined by the size of `mediumtext` on mysql, which is used to save dashboard data
type Service interface {
services.NamedService
registry.BackgroundService
registry.CanBeDisabled
}
type service struct {
services.NamedService
options *grafanaapiserveroptions.Options
restConfig *clientrest.Config
scheme *runtime.Scheme
codecs serializer.CodecFactory
cfg *setting.Cfg
features featuremgmt.FeatureToggles
log log.Logger
stopCh chan struct{}
stoppedCh chan error
db db.DB
rr routing.RouteRegister
handler http.Handler
builders []builder.APIGroupBuilder
tracing *tracing.TracingService
metrics prometheus.Registerer
authorizer *authorizer.GrafanaAuthorizer
serverLockService builder.ServerLockService
storageStatus dualwrite.Service
kvStore kvstore.KVStore
pluginClient plugins.Client
datasources datasource.ScopedPluginDatasourceProvider
contextProvider datasource.PluginContextWrapper
pluginStore pluginstore.Store
unified resource.ResourceClient
restConfigProvider RestConfigProvider
buildHandlerChainFuncFromBuilders builder.BuildHandlerChainFuncFromBuilders
aggregatorRunner aggregatorrunner.AggregatorRunner
}
func ProvideService(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
rr routing.RouteRegister,
tracing *tracing.TracingService,
serverLockService *serverlock.ServerLockService,
db db.DB,
kvStore kvstore.KVStore,
pluginClient plugins.Client,
datasources datasource.ScopedPluginDatasourceProvider,
contextProvider datasource.PluginContextWrapper,
pluginStore pluginstore.Store,
storageStatus dualwrite.Service,
unified resource.ResourceClient,
restConfigProvider RestConfigProvider,
buildHandlerChainFuncFromBuilders builder.BuildHandlerChainFuncFromBuilders,
eventualRestConfigProvider *eventualRestConfigProvider,
reg prometheus.Registerer,
aggregatorRunner aggregatorrunner.AggregatorRunner,
) (*service, error) {
scheme := builder.ProvideScheme()
codecs := builder.ProvideCodecFactory(scheme)
s := &service{
scheme: scheme,
codecs: codecs,
log: log.New(modules.GrafanaAPIServer),
cfg: cfg,
features: features,
rr: rr,
stopCh: make(chan struct{}),
builders: []builder.APIGroupBuilder{},
authorizer: authorizer.NewGrafanaAuthorizer(cfg),
tracing: tracing,
db: db, // For Unified storage
metrics: reg,
kvStore: kvStore,
pluginClient: pluginClient,
datasources: datasources,
contextProvider: contextProvider,
pluginStore: pluginStore,
serverLockService: serverLockService,
storageStatus: storageStatus,
unified: unified,
restConfigProvider: restConfigProvider,
buildHandlerChainFuncFromBuilders: buildHandlerChainFuncFromBuilders,
aggregatorRunner: aggregatorRunner,
}
// This will be used when running as a dskit service
service := services.NewBasicService(s.start, s.running, nil).WithName(modules.GrafanaAPIServer)
s.NamedService = servicetracing.NewServiceTracer(tracing.GetTracerProvider(), service)
// TODO: this is very hacky
// We need to register the routes in ProvideService to make sure
// the routes are registered before the Grafana HTTP server starts.
proxyHandler := func(k8sRoute routing.RouteRegister) {
handler := func(c *contextmodel.ReqContext) {
if err := s.AwaitRunning(c.Req.Context()); err != nil {
c.Resp.WriteHeader(http.StatusInternalServerError)
_, _ = c.Resp.Write([]byte(http.StatusText(http.StatusInternalServerError)))
return
}
if s.handler == nil {
c.Resp.WriteHeader(http.StatusNotFound)
_, _ = c.Resp.Write([]byte(http.StatusText(http.StatusNotFound)))
return
}
req := c.Req
if req.URL.Path == "" {
req.URL.Path = "/"
}
if c.SignedInUser != nil {
ctx := identity.WithRequester(req.Context(), c.SignedInUser)
req = req.WithContext(ctx)
}
resp := responsewriter.WrapForHTTP1Or2(c.Resp)
s.handler.ServeHTTP(resp, req)
}
k8sRoute.Any("/", middleware.ReqSignedIn, handler)
k8sRoute.Any("/*", middleware.ReqSignedIn, handler)
}
s.rr.Group("/apis", proxyHandler)
s.rr.Group("/livez", proxyHandler)
s.rr.Group("/readyz", proxyHandler)
s.rr.Group("/healthz", proxyHandler)
s.rr.Group("/openapi", proxyHandler)
s.rr.Group("/version", proxyHandler)
eventualRestConfigProvider.cfg = s
close(eventualRestConfigProvider.ready)
return s, nil
}
func (s *service) GetRestConfig(ctx context.Context) (*clientrest.Config, error) {
if err := s.AwaitRunning(ctx); err != nil {
return nil, fmt.Errorf("unable to get rest config: %w", err)
}
return s.restConfig, nil
}
func (s *service) IsDisabled() bool {
return false
}
// Run is an adapter for the BackgroundService interface.
func (s *service) Run(ctx context.Context) error {
if err := s.StartAsync(ctx); err != nil {
return err
}
if err := s.AwaitRunning(ctx); err != nil {
return err
}
return s.AwaitTerminated(ctx)
}
func (s *service) RegisterAPI(b builder.APIGroupBuilder) {
s.builders = append(s.builders, b)
}
// nolint:gocyclo
func (s *service) start(ctx context.Context) error {
// Get the list of groups the server will support
builders := s.builders
groupVersions := make([]schema.GroupVersion, 0, len(builders))
// Install schemas
for _, b := range builders {
gvs := builder.GetGroupVersions(b)
groupVersions = append(groupVersions, gvs...)
if len(gvs) == 0 {
return fmt.Errorf("no group versions found for builder %T", b)
}
if err := b.InstallSchema(s.scheme); err != nil {
return err
}
pvs := s.scheme.PrioritizedVersionsForGroup(gvs[0].Group)
for _, gv := range pvs {
if a, ok := b.(builder.APIGroupAuthorizer); ok {
auth := a.GetAuthorizer()
if auth != nil {
s.authorizer.Register(gv, auth)
}
}
}
}
o := grafanaapiserveroptions.NewOptions(s.codecs.LegacyCodec(groupVersions...))
err := applyGrafanaConfig(s.cfg, s.features, o)
if err != nil {
return err
}
if errs := o.Validate(); len(errs) != 0 {
// TODO: handle multiple errors
return errs[0]
}
serverConfig := genericapiserver.NewRecommendedConfig(s.codecs)
if err := o.ApplyTo(serverConfig); err != nil {
return err
}
serverConfig.Authorization.Authorizer = s.authorizer
serverConfig.Authentication.Authenticator = authenticator.NewAuthenticator(serverConfig.Authentication.Authenticator)
serverConfig.TracerProvider = s.tracing.GetTracerProvider()
// setup loopback transport for the aggregator server
transport := &grafanaapiserveroptions.RoundTripperFunc{Ready: make(chan struct{})}
serverConfig.LoopbackClientConfig.Transport = transport
serverConfig.LoopbackClientConfig.TLSClientConfig = clientrest.TLSClientConfig{}
serverConfig.MaxRequestBodyBytes = MaxRequestBodyBytes
var optsregister apistore.StorageOptionsRegister
if o.StorageOptions.StorageType == grafanaapiserveroptions.StorageTypeEtcd {
if err := o.RecommendedOptions.Etcd.Validate(); len(err) > 0 {
return err[0]
}
if err := o.RecommendedOptions.Etcd.ApplyTo(&serverConfig.Config); err != nil {
return err
}
} else {
getter := apistore.NewRESTOptionsGetterForClient(s.unified, o.RecommendedOptions.Etcd.StorageConfig, s.restConfigProvider)
optsregister = getter.RegisterOptions
// Use unified storage client
serverConfig.RESTOptionsGetter = getter
}
// Add OpenAPI specs for each group+version
err = builder.SetupConfig(
s.scheme,
serverConfig,
builders,
s.cfg.BuildStamp,
s.cfg.BuildVersion,
s.cfg.BuildCommit,
s.cfg.BuildBranch,
s.buildHandlerChainFuncFromBuilders,
)
if err != nil {
return err
}
notFoundHandler := notfoundhandler.New(s.codecs, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
// Create the server
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
if err != nil {
return err
}
// Install the API group+version
err = builder.InstallAPIs(s.scheme, s.codecs, server, serverConfig.RESTOptionsGetter, builders, o.StorageOptions,
// Required for the dual writer initialization
s.metrics,
request.GetNamespaceMapper(s.cfg),
kvstore.WithNamespace(s.kvStore, 0, "storage.dualwriting"), // NOTE: will be removed and replaced with the dual writer utility
s.serverLockService,
s.storageStatus,
optsregister,
s.features,
)
if err != nil {
return err
}
// stash the options for later use
s.options = o
delegate := server
var runningServer *genericapiserver.GenericAPIServer
isKubernetesAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator)
isDataplaneAggregatorEnabled := s.features.IsEnabledGlobally(featuremgmt.FlagDataplaneAggregator)
if isKubernetesAggregatorEnabled {
aggregatorServer, err := s.aggregatorRunner.Configure(s.options, serverConfig, delegate, s.scheme, builders)
if err != nil {
return err
}
// we are running with KubernetesAggregator FT set to true but with enterprise unlinked, handle this gracefully
if aggregatorServer != nil {
if !isDataplaneAggregatorEnabled {
runningServer, err = s.aggregatorRunner.Run(ctx, transport, s.stoppedCh)
if err != nil {
s.log.Error("aggregator runner failed to run", "error", err)
return err
}
} else {
delegate = aggregatorServer
}
} else {
// even though the FT is set to true, enterprise isn't linked
isKubernetesAggregatorEnabled = false
}
}
if isDataplaneAggregatorEnabled {
runningServer, err = s.startDataplaneAggregator(ctx, transport, serverConfig, delegate)
if err != nil {
return err
}
}
if !isDataplaneAggregatorEnabled && !isKubernetesAggregatorEnabled {
runningServer, err = s.startCoreServer(ctx, transport, server)
if err != nil {
return err
}
}
// only write kubeconfig in dev mode
if o.ExtraOptions.DevMode {
if err := ensureKubeConfig(runningServer.LoopbackClientConfig, o.StorageOptions.DataPath); err != nil {
return err
}
}
// used by the proxy wrapper registered in ProvideService
s.handler = runningServer.Handler
// used by local clients to make requests to the server
s.restConfig = runningServer.LoopbackClientConfig
return nil
}
func (s *service) startCoreServer(
ctx context.Context,
transport *grafanaapiserveroptions.RoundTripperFunc,
server *genericapiserver.GenericAPIServer,
) (*genericapiserver.GenericAPIServer, error) {
// setup the loopback transport and signal that it's ready.
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
// nolint:bodyclose
transport.Fn = grafanaresponsewriter.WrapHandler(server.Handler)
close(transport.Ready)
prepared := server.PrepareRun()
go func() {
s.stoppedCh <- prepared.RunWithContext(ctx)
}()
return server, nil
}
func (s *service) startDataplaneAggregator(
ctx context.Context,
transport *grafanaapiserveroptions.RoundTripperFunc,
serverConfig *genericapiserver.RecommendedConfig,
delegate *genericapiserver.GenericAPIServer,
) (*genericapiserver.GenericAPIServer, error) {
config := &dataplaneaggregator.Config{
GenericConfig: serverConfig,
ExtraConfig: dataplaneaggregator.ExtraConfig{
PluginClient: s.pluginClient,
PluginContextProvider: &pluginContextProvider{
pluginStore: s.pluginStore,
datasources: s.datasources,
contextProvider: s.contextProvider,
},
},
}
if err := s.options.GrafanaAggregatorOptions.ApplyTo(config, s.options.RecommendedOptions.Etcd); err != nil {
return nil, err
}
completedConfig := config.Complete()
aggregatorServer, err := completedConfig.NewWithDelegate(delegate)
if err != nil {
return nil, err
}
// setup the loopback transport for the aggregator server and signal that it's ready
// ignore the lint error because the response is passed directly to the client,
// so the client will be responsible for closing the response body.
// nolint:bodyclose
transport.Fn = grafanaresponsewriter.WrapHandler(aggregatorServer.GenericAPIServer.Handler)
close(transport.Ready)
prepared, err := aggregatorServer.PrepareRun()
if err != nil {
return nil, err
}
go func() {
s.stoppedCh <- prepared.RunWithContext(ctx)
}()
return aggregatorServer.GenericAPIServer, nil
}
func (s *service) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config {
return &clientrest.Config{
Transport: &grafanaapiserveroptions.RoundTripperFunc{
Fn: func(req *http.Request) (*http.Response, error) {
if err := s.AwaitRunning(req.Context()); err != nil {
return nil, err
}
ctx := identity.WithRequester(req.Context(), c.SignedInUser)
wrapped := grafanaresponsewriter.WrapHandler(s.handler)
return wrapped(req.WithContext(ctx))
},
},
}
}
func (s *service) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := s.AwaitRunning(r.Context()); err != nil {
return
}
s.handler.ServeHTTP(w, r)
}
func (s *service) running(ctx context.Context) error {
select {
case err := <-s.stoppedCh:
if err != nil {
return err
}
case <-ctx.Done():
return ctx.Err()
}
return nil
}
func ensureKubeConfig(restConfig *clientrest.Config, dir string) error {
return clientcmd.WriteToFile(
utils.FormatKubeConfig(restConfig),
path.Join(dir, "grafana.kubeconfig"),
)
}
type pluginContextProvider struct {
pluginStore pluginstore.Store
datasources datasource.ScopedPluginDatasourceProvider
contextProvider datasource.PluginContextWrapper
}
func (p *pluginContextProvider) GetPluginContext(ctx context.Context, pluginID string, uid string) (backend.PluginContext, error) {
all := p.pluginStore.Plugins(ctx)
var datasourceProvider datasource.PluginDatasourceProvider
for _, plugin := range all {
if plugin.ID == pluginID {
datasourceProvider = p.datasources.GetDatasourceProvider(plugin.JSONData)
}
}
if datasourceProvider == nil {
return backend.PluginContext{}, fmt.Errorf("plugin not found")
}
s, err := datasourceProvider.GetInstanceSettings(ctx, uid)
if err != nil {
return backend.PluginContext{}, err
}
return p.contextProvider.PluginContextForDataSource(ctx, s)
}