coremodels: Combine static and generic registries (#53246)

* Stop generating non-dynamic registry code

* Remove generic, errors, s/static/base/

* Sort during codegen, not runtime

* Not a method call

* Precisiate a comment

* Remove generic registry, fix assignability test
This commit is contained in:
sam boyer
2022-08-04 00:04:54 +04:00
committed by GitHub
parent d54e55ea9a
commit b11f66b4bb
9 changed files with 101 additions and 247 deletions
+1 -1
View File
@@ -353,7 +353,7 @@ func (hs *HTTPServer) PostDashboard(c *models.ReqContext) response.Response {
} }
if hs.Features.IsEnabled(featuremgmt.FlagValidateDashboardsOnSave) { if hs.Features.IsEnabled(featuremgmt.FlagValidateDashboardsOnSave) {
cm := hs.CoremodelStaticRegistry.Dashboard() cm := hs.Coremodels.Dashboard()
// Ideally, coremodel validation calls would be integrated into the web // Ideally, coremodel validation calls would be integrated into the web
// framework. But this does the job for now. // framework. But this does the job for now.
+8 -18
View File
@@ -57,8 +57,8 @@ func TestGetHomeDashboard(t *testing.T) {
SQLStore: mockstore.NewSQLStoreMock(), SQLStore: mockstore.NewSQLStoreMock(),
preferenceService: prefService, preferenceService: prefService,
dashboardVersionService: dashboardVersionService, dashboardVersionService: dashboardVersionService,
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
tests := []struct { tests := []struct {
name string name string
@@ -141,8 +141,8 @@ func TestDashboardAPIEndpoint(t *testing.T) {
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
DashboardService: dashboardService, DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService, dashboardVersionService: fakeDashboardVersionService,
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
setUp := func() { setUp := func() {
viewerRole := models.ROLE_VIEWER viewerRole := models.ROLE_VIEWER
@@ -262,8 +262,8 @@ func TestDashboardAPIEndpoint(t *testing.T) {
DashboardService: dashboardService, DashboardService: dashboardService,
dashboardVersionService: fakeDashboardVersionService, dashboardVersionService: fakeDashboardVersionService,
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
setUp := func() { setUp := func() {
origCanEdit := setting.ViewersCanEdit origCanEdit := setting.ViewersCanEdit
@@ -903,8 +903,8 @@ func TestDashboardAPIEndpoint(t *testing.T) {
AccessControl: accesscontrolmock.New(), AccessControl: accesscontrolmock.New(),
DashboardService: dashboardService, DashboardService: dashboardService,
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
hs.callGetDashboard(sc) hs.callGetDashboard(sc)
assert.Equal(t, 200, sc.resp.Code) assert.Equal(t, 200, sc.resp.Code)
@@ -958,8 +958,8 @@ func getDashboardShouldReturn200WithConfig(t *testing.T, sc *scenarioContext, pr
), ),
DashboardService: dashboardService, DashboardService: dashboardService,
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
hs.callGetDashboard(sc) hs.callGetDashboard(sc)
@@ -1024,8 +1024,8 @@ func postDashboardScenario(t *testing.T, desc string, url string, routePattern s
DashboardService: dashboardService, DashboardService: dashboardService,
folderService: folderService, folderService: folderService,
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
@@ -1057,8 +1057,8 @@ func postDiffScenario(t *testing.T, desc string, url string, routePattern string
SQLStore: sqlmock, SQLStore: sqlmock,
dashboardVersionService: fakeDashboardVersionService, dashboardVersionService: fakeDashboardVersionService,
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response { sc.defaultHandler = routing.Wrap(func(c *models.ReqContext) response.Response {
@@ -1096,8 +1096,8 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
SQLStore: sqlStore, SQLStore: sqlStore,
Features: featuremgmt.WithFeatures(), Features: featuremgmt.WithFeatures(),
dashboardVersionService: fakeDashboardVersionService, dashboardVersionService: fakeDashboardVersionService,
Coremodels: registry.NewBase(),
} }
hs.CoremodelStaticRegistry, hs.CoremodelRegistry = setupDashboardCoremodel(t)
sc := setupScenarioContext(t, url) sc := setupScenarioContext(t, url)
sc.sqlStore = sqlStore sc.sqlStore = sqlStore
@@ -1121,16 +1121,6 @@ func restoreDashboardVersionScenario(t *testing.T, desc string, url string, rout
}) })
} }
func setupDashboardCoremodel(t *testing.T) (*registry.Static, *registry.Generic) {
// TODO abstract and generalize this further for wider reuse
t.Helper()
sreg, err := registry.ProvideStatic()
require.NoError(t, err)
greg, err := registry.ProvideGeneric()
require.NoError(t, err)
return sreg, greg
}
func (sc *scenarioContext) ToJSON() *simplejson.Json { func (sc *scenarioContext) ToJSON() *simplejson.Json {
result := simplejson.New() result := simplejson.New()
err := json.NewDecoder(sc.resp.Body).Decode(result) err := json.NewDecoder(sc.resp.Body).Decode(result)
+3 -5
View File
@@ -171,10 +171,9 @@ type HTTPServer struct {
dashboardVersionService dashver.Service dashboardVersionService dashver.Service
PublicDashboardsApi *publicdashboardsApi.Api PublicDashboardsApi *publicdashboardsApi.Api
starService star.Service starService star.Service
Coremodels *registry.Base
playlistService playlist.Service playlistService playlist.Service
apiKeyService apikey.Service apiKeyService apikey.Service
CoremodelRegistry *registry.Generic
CoremodelStaticRegistry *registry.Static
kvStore kvstore.KVStore kvStore kvstore.KVStore
secretsMigrator secrets.Migrator secretsMigrator secrets.Migrator
userService user.Service userService user.Service
@@ -211,7 +210,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service, avatarCacheServer *avatar.AvatarCacheServer, preferenceService pref.Service,
teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService, teamsPermissionsService accesscontrol.TeamPermissionsService, folderPermissionsService accesscontrol.FolderPermissionsService,
dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service,
starService star.Service, csrfService csrf.Service, coremodelRegistry *registry.Generic, coremodelStaticRegistry *registry.Static, starService star.Service, csrfService csrf.Service, coremodels *registry.Base,
playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck, playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, remoteSecretsCheck secretsKV.UseRemoteSecretsPluginCheck,
publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) { publicDashboardsApi *publicdashboardsApi.Api, userService user.Service) (*HTTPServer, error) {
web.Env = cfg.Env web.Env = cfg.Env
@@ -294,10 +293,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
dashboardPermissionsService: dashboardPermissionsService, dashboardPermissionsService: dashboardPermissionsService,
dashboardVersionService: dashboardVersionService, dashboardVersionService: dashboardVersionService,
starService: starService, starService: starService,
Coremodels: coremodels,
playlistService: playlistService, playlistService: playlistService,
apiKeyService: apiKeyService, apiKeyService: apiKeyService,
CoremodelRegistry: coremodelRegistry,
CoremodelStaticRegistry: coremodelStaticRegistry,
kvStore: kvStore, kvStore: kvStore,
PublicDashboardsApi: publicDashboardsApi, PublicDashboardsApi: publicDashboardsApi,
secretsMigrator: secretsMigrator, secretsMigrator: secretsMigrator,
+20 -55
View File
@@ -281,8 +281,8 @@ func (m modelReplacer) replacePrefix(str string) string {
return str return str
} }
// GenerateCoremodelRegistry produces Go files that define a static registry // GenerateCoremodelRegistry produces Go files that define a registry with
// with references to all the Go code that is expected to be generated from the // references to all the Go code that is expected to be generated from the
// provided lineages. // provided lineages.
func GenerateCoremodelRegistry(path string, ecl []*ExtractedLineage) (WriteDiffer, error) { func GenerateCoremodelRegistry(path string, ecl []*ExtractedLineage) (WriteDiffer, error) {
var cml []tplVars var cml []tplVars
@@ -409,6 +409,7 @@ var tmplRegistry = template.Must(template.New("registry").Parse(`
package registry package registry
import ( import (
"fmt"
"sync" "sync"
"github.com/google/wire" "github.com/google/wire"
@@ -419,25 +420,17 @@ import (
"github.com/grafana/thema" "github.com/grafana/thema"
) )
// CoremodelSet contains all of the wire-style providers related to coremodels. // Base is a registry of coremodel.Interface. It provides two modes for accessing
var CoremodelSet = wire.NewSet( // coremodels: individually via literal named methods, or as a slice returned from All().
ProvideStatic, //
ProvideGeneric, // Prefer the individual named methods for use cases where the particular coremodel(s) that
) // are needed are known to the caller. For example, a dashboard linter can know that it
// specifically wants the dashboard coremodel.
var ( //
staticOnce sync.Once // Prefer All() when performing operations generically across all coremodels. For example,
defaultStatic *Static // a validation HTTP middleware for any coremodel-schematized object type.
defaultStaticErr error type Base struct {
all []coremodel.Interface
genericOnce sync.Once
defaultGeneric *Generic
defaultGenericErr error
)
// Static is a registry that provides access to individual coremodels via
// explicit method calls, to aid with static analysis.
type Static struct {
{{- range .Coremodels }} {{- range .Coremodels }}
{{ .Name }} *{{ .Name }}.Coremodel{{end}} {{ .Name }} *{{ .Name }}.Coremodel{{end}}
} }
@@ -451,51 +444,23 @@ var (
{{range .Coremodels }} {{range .Coremodels }}
// {{ .TitleName }} returns the {{ .Name }} coremodel. The return value is guaranteed to // {{ .TitleName }} returns the {{ .Name }} coremodel. The return value is guaranteed to
// implement coremodel.Interface. // implement coremodel.Interface.
func (s *Static) {{ .TitleName }}() *{{ .Name }}.Coremodel { func (s *Base) {{ .TitleName }}() *{{ .Name }}.Coremodel {
return s.{{ .Name }} return s.{{ .Name }}
} }
{{end}} {{end}}
func provideStatic(lib *thema.Library) (*Static, error) { func doProvideBase(lib thema.Library) *Base {
if lib == nil {
staticOnce.Do(func() {
defaultStatic, defaultStaticErr = doProvideStatic(cuectx.ProvideThemaLibrary())
})
return defaultStatic, defaultStaticErr
}
return doProvideStatic(*lib)
}
func doProvideStatic(lib thema.Library) (*Static, error) {
var err error var err error
reg := &Static{} reg := &Base{}
{{range .Coremodels }} {{range .Coremodels }}
reg.{{ .Name }}, err = {{ .Name }}.New(lib) reg.{{ .Name }}, err = {{ .Name }}.New(lib)
if err != nil { if err != nil {
return nil, err panic(fmt.Sprintf("error while initializing {{ .Name }} coremodel: %s", err))
} }
reg.all = append(reg.all, reg.{{ .Name }})
{{end}} {{end}}
return reg, nil return reg
}
func provideGeneric() (*Generic, error) {
ereg, err := provideStatic(nil)
if err != nil {
return nil, err
}
genericOnce.Do(func() {
defaultGeneric, defaultGenericErr = doProvideGeneric(ereg)
})
return defaultGeneric, defaultGenericErr
}
func doProvideGeneric(ereg *Static) (*Generic, error) {
return NewRegistry({{ range .Coremodels }}
ereg.{{ .TitleName }}(),{{ end }}
)
} }
`)) `))
+4
View File
@@ -9,6 +9,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"cuelang.org/go/cue/cuecontext" "cuelang.org/go/cue/cuecontext"
@@ -59,6 +60,9 @@ func main() {
lins = append(lins, lin) lins = append(lins, lin)
} }
} }
sort.Slice(lins, func(i, j int) bool {
return lins[i].Lineage.Name() < lins[j].Lineage.Name()
})
wd := gcgen.NewWriteDiffer() wd := gcgen.NewWriteDiffer()
for _, ls := range lins { for _, ls := range lins {
@@ -8,12 +8,9 @@ import (
) )
func TestSchemaAssignability(t *testing.T) { func TestSchemaAssignability(t *testing.T) {
reg, err := registry.ProvideGeneric() reg := registry.NewBase()
if err != nil {
t.Fatal(err)
}
for _, cm := range reg.List() { for _, cm := range reg.All() {
tcm := cm tcm := cm
t.Run(tcm.Lineage().Name(), func(t *testing.T) { t.Run(tcm.Lineage().Name(), func(t *testing.T) {
err := thema.AssignableTo(tcm.CurrentSchema(), tcm.GoType()) err := thema.AssignableTo(tcm.CurrentSchema(), tcm.GoType())
+45 -26
View File
@@ -1,40 +1,59 @@
package registry package registry
import ( import (
"sync"
"github.com/google/wire"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema" "github.com/grafana/thema"
) )
// ProvideStatic provides access to individual coremodels via explicit method calls. // CoremodelSet contains all of the wire-style providers related to coremodels.
var CoremodelSet = wire.NewSet(
NewBase,
)
// NewBase provides a registry of all coremodels, without any composition of
// plugin-defined schemas.
// //
// Prefer this to the ProvideGeneric type when your code works with known, // The returned registry will use the default Grafana thema.Library, defined in
// specific coremodels(s), rather than generically across all of them. This // pkg/cuectx. If you need control over the thema.Library used by the coremodel
// allows standard Go static analysis tools to determine which code is depending // lineages, use NewBaseWithLib instead.
// on particular coremodels. func NewBase() *Base {
// return provideBase(nil)
// This will use the default Grafana thema.Library, defined in pkg/cuectx, which
// will avoid duplicate parsing of Thema CUE schemas. If you need control over the
// thema.Library in use, use ProvideStaticWithLib instead.
func ProvideStatic() (*Static, error) {
return provideStatic(nil)
} }
// ProvideStaticWithLib is the same as ProvideStatic, but // NewBaseWithLib is the same as NewBase, but allows control over the
// allows control over the thema.Library used to initialize the underlying // thema.Library used to initialize the underlying coremodels.
// coremodels.
// //
// Prefer ProvideStatic unless you absolutely need this control. // Prefer NewBase unless you absolutely need this control.
func ProvideStaticWithLib(lib thema.Library) (*Static, error) { func NewBaseWithLib(lib thema.Library) *Base {
return provideStatic(&lib) return provideBase(&lib)
} }
// ProvideGeneric provides a simple Generic registry of all coremodels. var (
// baseOnce sync.Once
// Prefer this to the static ProvideStatic when your code needs to defaultBase *Base
// work with all coremodels generically, rather than specific coremodels. )
func ProvideGeneric() (*Generic, error) {
return provideGeneric() func provideBase(lib *thema.Library) *Base {
if lib == nil {
baseOnce.Do(func() {
defaultBase = doProvideBase(cuectx.ProvideThemaLibrary())
})
return defaultBase
}
return doProvideBase(*lib)
} }
// NOTE - no ProvideRegistryWithLib is defined because there are no anticipated // All returns a slice of all registered coremodels.
// cases where a caller would need to operate generically across all coremodels, //
// and control the library they're initialized with. If that changes, add one. // Prefer this method when operating generically across all coremodels.
//
// The returned slice is sorted lexicographically by coremodel name. It should
// not be modified.
func (s *Base) All() []coremodel.Interface {
return s.all
}
@@ -1,80 +0,0 @@
package registry
import (
"errors"
"fmt"
"sync"
"github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema"
)
var (
// ErrModelAlreadyRegistered is returned when trying to register duplicate model to Generic.
ErrModelAlreadyRegistered = errors.New("error registering duplicate model")
)
// Generic is a registry of coremodel instances. It is intended for use in cases where
// generic operations limited to coremodel.Interface are being performed.
type Generic struct {
lock sync.RWMutex
models []coremodel.Interface
modelIdx map[string]coremodel.Interface
}
// NewRegistry returns a new Generic with the provided coremodel instances.
func NewRegistry(models ...coremodel.Interface) (*Generic, error) {
r := &Generic{
models: make([]coremodel.Interface, 0, len(models)),
modelIdx: make(map[string]coremodel.Interface, len(models)),
}
if err := r.addModels(models); err != nil {
return nil, err
}
return r, nil
}
// Register adds coremodels to the Generic.
func (r *Generic) Register(models ...coremodel.Interface) error {
return r.addModels(models)
}
// List returns all coremodels registered in this Generic.
func (r *Generic) List() []coremodel.Interface {
r.lock.RLock()
defer r.lock.RUnlock()
return r.models
}
func (r *Generic) addModels(models []coremodel.Interface) error {
r.lock.Lock()
defer r.lock.Unlock()
// Update model index and return an error if trying to register a duplicate.
for _, m := range models {
k := m.Lineage().Name()
// Ensure assignability first. TODO will this blow up for dashboards?
if err := thema.AssignableTo(m.CurrentSchema(), m.GoType()); err != nil {
return fmt.Errorf("%s schema version %v not assignable to provided Go type: %w", k, m.CurrentSchema().Version(), err)
}
if _, ok := r.modelIdx[k]; ok {
return ErrModelAlreadyRegistered
}
r.modelIdx[k] = m
}
// Remake model list.
// TODO: this can be more performant (proper resizing, maybe single loop with index building, etc.).
r.models = r.models[:0]
for _, m := range r.modelIdx {
r.models = append(r.models, m)
}
return nil
}
@@ -6,35 +6,24 @@
package registry package registry
import ( import (
"sync" "fmt"
"github.com/google/wire"
"github.com/grafana/grafana/pkg/coremodel/dashboard" "github.com/grafana/grafana/pkg/coremodel/dashboard"
"github.com/grafana/grafana/pkg/cuectx"
"github.com/grafana/grafana/pkg/framework/coremodel" "github.com/grafana/grafana/pkg/framework/coremodel"
"github.com/grafana/thema" "github.com/grafana/thema"
) )
// CoremodelSet contains all of the wire-style providers related to coremodels. // Base is a registry of coremodel.Interface. It provides two modes for accessing
var CoremodelSet = wire.NewSet( // coremodels: individually via literal named methods, or as a slice returned from All().
ProvideStatic, //
ProvideGeneric, // Prefer the individual named methods for use cases where the particular coremodel(s) that
) // are needed are known to the caller. For example, a dashboard linter can know that it
// specifically wants the dashboard coremodel.
var ( //
staticOnce sync.Once // Prefer All() when performing operations generically across all coremodels. For example,
defaultStatic *Static // a validation HTTP middleware for any coremodel-schematized object type.
defaultStaticErr error type Base struct {
all []coremodel.Interface
genericOnce sync.Once
defaultGeneric *Generic
defaultGenericErr error
)
// Static is a registry that provides access to individual coremodels via
// explicit method calls, to aid with static analysis.
type Static struct {
dashboard *dashboard.Coremodel dashboard *dashboard.Coremodel
} }
@@ -45,47 +34,19 @@ var (
// Dashboard returns the dashboard coremodel. The return value is guaranteed to // Dashboard returns the dashboard coremodel. The return value is guaranteed to
// implement coremodel.Interface. // implement coremodel.Interface.
func (s *Static) Dashboard() *dashboard.Coremodel { func (s *Base) Dashboard() *dashboard.Coremodel {
return s.dashboard return s.dashboard
} }
func provideStatic(lib *thema.Library) (*Static, error) { func doProvideBase(lib thema.Library) *Base {
if lib == nil {
staticOnce.Do(func() {
defaultStatic, defaultStaticErr = doProvideStatic(cuectx.ProvideThemaLibrary())
})
return defaultStatic, defaultStaticErr
}
return doProvideStatic(*lib)
}
func doProvideStatic(lib thema.Library) (*Static, error) {
var err error var err error
reg := &Static{} reg := &Base{}
reg.dashboard, err = dashboard.New(lib) reg.dashboard, err = dashboard.New(lib)
if err != nil { if err != nil {
return nil, err panic(fmt.Sprintf("error while initializing dashboard coremodel: %s", err))
} }
reg.all = append(reg.all, reg.dashboard)
return reg, nil return reg
}
func provideGeneric() (*Generic, error) {
ereg, err := provideStatic(nil)
if err != nil {
return nil, err
}
genericOnce.Do(func() {
defaultGeneric, defaultGenericErr = doProvideGeneric(ereg)
})
return defaultGeneric, defaultGenericErr
}
func doProvideGeneric(ereg *Static) (*Generic, error) {
return NewRegistry(
ereg.Dashboard(),
)
} }