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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user