App Platform: Remove mutable globals (#102962)

* App Platform: Remove mutable globals

* chore: clarify why this exists

* fix: support multi-tenant mode

* refactor: call builder providers directly

* CI: Force re-build
This commit is contained in:
Mariell Hoversholm
2025-03-27 15:46:09 +01:00
committed by GitHub
parent 031a047565
commit d0d7078953
29 changed files with 199 additions and 108 deletions
@@ -11,7 +11,6 @@ import (
"k8s.io/apiserver/pkg/server"
"k8s.io/kube-openapi/pkg/common"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
)
@@ -60,7 +59,11 @@ func TestAddPostStartHooks(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := server.NewRecommendedConfig(apiserver.Codecs)
t.Parallel()
scheme := builder.ProvideScheme()
codecs := builder.ProvideCodecFactory(scheme)
config := server.NewRecommendedConfig(codecs)
err := builder.AddPostStartHooks(config, tt.builders)
if tt.wantErr {
require.Error(t, err)
+32
View File
@@ -0,0 +1,32 @@
package builder
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
)
func ProvideScheme() *runtime.Scheme {
unversionedVersion := schema.GroupVersion{Group: "", Version: "v1"}
unversionedTypes := []runtime.Object{
&metav1.Status{},
&metav1.WatchEvent{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
&metav1.PartialObjectMetadata{},
&metav1.PartialObjectMetadataList{},
}
scheme := runtime.NewScheme()
// we need to add the options to empty v1
metav1.AddToGroupVersion(scheme, schema.GroupVersion{Group: "", Version: "v1"})
scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
return scheme
}
func ProvideCodecFactory(scheme *runtime.Scheme) serializer.CodecFactory {
return serializer.NewCodecFactory(scheme)
}
+86
View File
@@ -0,0 +1,86 @@
package apiserver
import (
"context"
"errors"
"net/http"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
clientrest "k8s.io/client-go/rest"
)
type RestConfigProvider interface {
GetRestConfig(context.Context) (*clientrest.Config, error)
}
type RestConfigProviderFunc func(context.Context) (*clientrest.Config, error)
func (f RestConfigProviderFunc) GetRestConfig(ctx context.Context) (*clientrest.Config, error) {
return f(ctx)
}
// WithoutRestConfig is a RestConfigProvider that always returns an error.
// This is intended for use in unit tests where the rest config is not needed.
var WithoutRestConfig = RestConfigProviderFunc(func(context.Context) (*clientrest.Config, error) {
return nil, errors.New("rest config will not be available (unit test?)")
})
type DirectRestConfigProvider interface {
// GetDirectRestConfig returns a k8s client configuration that will use the same
// logged in user as the current request context. This is useful when
// creating clients that map legacy API handlers to k8s backed services
GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config
// This can be used to rewrite incoming requests to path now supported under /apis
DirectlyServeHTTP(w http.ResponseWriter, r *http.Request)
}
func ProvideEventualRestConfigProvider() *eventualRestConfigProvider {
return &eventualRestConfigProvider{
ready: make(chan struct{}),
}
}
var (
_ RestConfigProvider = (*eventualRestConfigProvider)(nil)
_ DirectRestConfigProvider = (*eventualRestConfigProvider)(nil)
)
// eventualRestConfigProvider is a RestConfigProvider that will not return a rest config until the ready channel is closed.
// This exists to alleviate a circular dependency between the apiserver.server's dependencies and their dependencies wanting a rest config.
// Importantly, this is handled by wire as opposed to a mutable global.
type eventualRestConfigProvider struct {
// When this channel is closed, we can start returning the rest config.
ready chan struct{}
cfg interface {
RestConfigProvider
DirectRestConfigProvider
}
}
func (e *eventualRestConfigProvider) GetRestConfig(ctx context.Context) (*clientrest.Config, error) {
select {
case <-e.ready:
return e.cfg.GetRestConfig(ctx)
case <-ctx.Done():
return nil, ctx.Err()
}
}
func (e *eventualRestConfigProvider) GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config {
select {
case <-e.ready:
return e.cfg.GetDirectRestConfig(c)
case <-c.Req.Context().Done():
return nil
}
}
func (e *eventualRestConfigProvider) DirectlyServeHTTP(w http.ResponseWriter, r *http.Request) {
select {
case <-e.ready:
e.cfg.DirectlyServeHTTP(w, r)
case <-r.Context().Done():
// Do nothing: the request has been cancelled.
}
}
+16 -72
View File
@@ -7,7 +7,6 @@ import (
"path"
"github.com/prometheus/client_golang/prometheus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -58,80 +57,23 @@ var (
_ RestConfigProvider = (*service)(nil)
_ registry.BackgroundService = (*service)(nil)
_ registry.CanBeDisabled = (*service)(nil)
Scheme = runtime.NewScheme()
Codecs = serializer.NewCodecFactory(Scheme)
unversionedVersion = schema.GroupVersion{Group: "", Version: "v1"}
unversionedTypes = []runtime.Object{
&metav1.Status{},
&metav1.WatchEvent{},
&metav1.APIVersions{},
&metav1.APIGroupList{},
&metav1.APIGroup{},
&metav1.APIResourceList{},
&metav1.PartialObjectMetadata{},
&metav1.PartialObjectMetadataList{},
}
// internal provider of the package level client Config
restConfig RestConfigProvider
ready = make(chan struct{})
)
const MaxRequestBodyBytes = 16 * 1024 * 1024 // 16MB - determined by the size of `mediumtext` on mysql, which is used to save dashboard data
func init() {
// we need to add the options to empty v1
metav1.AddToGroupVersion(Scheme, schema.GroupVersion{Group: "", Version: "v1"})
Scheme.AddUnversionedTypes(unversionedVersion, unversionedTypes...)
}
// ClearRestConfig clears the package level restConfig.
// This is intended to be used in tests only.
//
// TODO: Refactor such that there is no global state.
func ClearRestConfig() {
restConfig = nil
ready = make(chan struct{})
}
// GetRestConfig return a client Config mounted at package level
// This resolves circular dependency issues between apiserver, authz,
// and Folder Service.
// The client Config gets initialized during the first call to
// ProvideService.
// Any call to GetRestConfig will block until we have a restConfig available
func GetRestConfig(ctx context.Context) (*clientrest.Config, error) {
<-ready
return restConfig.GetRestConfig(ctx)
}
type Service interface {
services.NamedService
registry.BackgroundService
registry.CanBeDisabled
}
type RestConfigProvider interface {
GetRestConfig(context.Context) (*clientrest.Config, error)
}
type DirectRestConfigProvider interface {
// GetDirectRestConfig returns a k8s client configuration that will use the same
// logged in user as the current request context. This is useful when
// creating clients that map legacy API handlers to k8s backed services
GetDirectRestConfig(c *contextmodel.ReqContext) *clientrest.Config
// This can be used to rewrite incoming requests to path now supported under /apis
DirectlyServeHTTP(w http.ResponseWriter, r *http.Request)
}
type service struct {
services.NamedService
options *grafanaapiserveroptions.Options
restConfig *clientrest.Config
scheme *runtime.Scheme
codecs serializer.CodecFactory
cfg *setting.Cfg
features featuremgmt.FeatureToggles
@@ -177,8 +119,13 @@ func ProvideService(
storageStatus dualwrite.Service,
unified resource.ResourceClient,
buildHandlerChainFuncFromBuilders builder.BuildHandlerChainFuncFromBuilders,
eventualRestConfigProvider *eventualRestConfigProvider,
) (*service, error) {
scheme := builder.ProvideScheme()
codecs := builder.ProvideCodecFactory(scheme)
s := &service{
scheme: scheme,
codecs: codecs,
log: log.New(modules.GrafanaAPIServer),
cfg: cfg,
features: features,
@@ -244,11 +191,8 @@ func ProvideService(
s.rr.Group("/openapi", proxyHandler)
s.rr.Group("/version", proxyHandler)
// only set the package level restConfig once
if restConfig == nil {
restConfig = s
close(ready)
}
eventualRestConfigProvider.cfg = s
close(eventualRestConfigProvider.ready)
return s, nil
}
@@ -293,10 +237,10 @@ func (s *service) start(ctx context.Context) error {
if len(gvs) == 0 {
return fmt.Errorf("no group versions found for builder %T", b)
}
if err := b.InstallSchema(Scheme); err != nil {
if err := b.InstallSchema(s.scheme); err != nil {
return err
}
pvs := Scheme.PrioritizedVersionsForGroup(gvs[0].Group)
pvs := s.scheme.PrioritizedVersionsForGroup(gvs[0].Group)
for j, gv := range pvs {
if s.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAggregator) {
@@ -313,7 +257,7 @@ func (s *service) start(ctx context.Context) error {
}
}
o := grafanaapiserveroptions.NewOptions(Codecs.LegacyCodec(groupVersions...))
o := grafanaapiserveroptions.NewOptions(s.codecs.LegacyCodec(groupVersions...))
err := applyGrafanaConfig(s.cfg, s.features, o)
if err != nil {
return err
@@ -332,7 +276,7 @@ func (s *service) start(ctx context.Context) error {
}
}
serverConfig := genericapiserver.NewRecommendedConfig(Codecs)
serverConfig := genericapiserver.NewRecommendedConfig(s.codecs)
if err := o.ApplyTo(serverConfig); err != nil {
return err
}
@@ -365,7 +309,7 @@ func (s *service) start(ctx context.Context) error {
// Add OpenAPI specs for each group+version
err = builder.SetupConfig(
Scheme,
s.scheme,
serverConfig,
builders,
s.cfg.BuildStamp,
@@ -378,7 +322,7 @@ func (s *service) start(ctx context.Context) error {
return err
}
notFoundHandler := notfoundhandler.New(Codecs, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
notFoundHandler := notfoundhandler.New(s.codecs, genericapifilters.NoMuxAndDiscoveryIncompleteKey)
// Create the server
server, err := serverConfig.Complete().New("grafana-apiserver", genericapiserver.NewEmptyDelegateWithCustomHandler(notFoundHandler))
@@ -387,7 +331,7 @@ func (s *service) start(ctx context.Context) error {
}
// Install the API group+version
err = builder.InstallAPIs(Scheme, Codecs, server, serverConfig.RESTOptionsGetter, builders, o.StorageOptions,
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"),
s.serverLockService,
+3 -2
View File
@@ -7,9 +7,10 @@ import (
)
var WireSet = wire.NewSet(
ProvideEventualRestConfigProvider,
wire.Bind(new(RestConfigProvider), new(*eventualRestConfigProvider)),
wire.Bind(new(DirectRestConfigProvider), new(*eventualRestConfigProvider)),
ProvideService,
wire.Bind(new(RestConfigProvider), new(*service)),
wire.Bind(new(Service), new(*service)),
wire.Bind(new(DirectRestConfigProvider), new(*service)),
wire.Bind(new(builder.APIRegistrar), new(*service)),
)