FS: Call IndexDataHooks for custom version string (#112670)
* Add enterprise hooks * wip... * undo * update wire gen * remove old hook thing * move build info into seperate func * align fs context middleware with grafana, setting SignedInUser * Call IndexDataHooks to get modified build info * update tests * go workspace * idk, reset workspace files or whatever * conditionally mount license * support loading decoupled plugins from cdn --------- Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
co-authored by
Ashley Harrison
parent
71d10a3fa3
commit
bb6d7d02c7
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/authz"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/frontend"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -46,8 +47,9 @@ func NewModule(opts Options,
|
||||
license licensing.Licensing,
|
||||
moduleRegisterer ModuleRegisterer,
|
||||
storageBackend resource.StorageBackend, // Ensures unified storage backend is initialized
|
||||
hooksService *hooks.HooksService,
|
||||
) (*ModuleServer, error) {
|
||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend)
|
||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, reg, promGatherer, license, moduleRegisterer, storageBackend, hooksService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -70,6 +72,7 @@ func newModuleServer(opts Options,
|
||||
license licensing.Licensing,
|
||||
moduleRegisterer ModuleRegisterer,
|
||||
storageBackend resource.StorageBackend,
|
||||
hooksService *hooks.HooksService,
|
||||
) (*ModuleServer, error) {
|
||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||
|
||||
@@ -93,6 +96,7 @@ func newModuleServer(opts Options,
|
||||
license: license,
|
||||
moduleRegisterer: moduleRegisterer,
|
||||
storageBackend: storageBackend,
|
||||
hooksService: hooksService,
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -134,6 +138,7 @@ type ModuleServer struct {
|
||||
|
||||
// moduleRegisterer allows registration of modules provided by other builds (e.g. enterprise).
|
||||
moduleRegisterer ModuleRegisterer
|
||||
hooksService *hooks.HooksService
|
||||
}
|
||||
|
||||
// init initializes the server and its services.
|
||||
@@ -205,7 +210,7 @@ func (s *ModuleServer) Run() error {
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.FrontendServer, func() (services.Service, error) {
|
||||
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license)
|
||||
return frontend.ProvideFrontendService(s.cfg, s.features, s.promGatherer, s.registerer, s.license, s.hooksService)
|
||||
})
|
||||
|
||||
m.RegisterModule(modules.OperatorServer, s.initOperatorServer)
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
"github.com/grafana/grafana/pkg/modules"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore/sqlutil"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||
@@ -330,8 +332,10 @@ func initModuleServerForTest(
|
||||
apiOpts api.ServerOptions,
|
||||
) testModuleServer {
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
hooksService := hooks.ProvideService()
|
||||
license := &licensing.OSSLicensingService{}
|
||||
|
||||
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, nil, ProvideNoopModuleRegisterer(), nil)
|
||||
ms, err := NewModule(opts, apiOpts, featuremgmt.WithFeatures(featuremgmt.FlagUnifiedStorageSearch), cfg, nil, nil, prometheus.NewRegistry(), prometheus.DefaultGatherer, tracer, license, ProvideNoopModuleRegisterer(), nil, hooksService)
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := grpc.NewClient(cfg.GRPCServer.Address,
|
||||
|
||||
@@ -1644,7 +1644,7 @@ func InitializeModuleServer(cfg *setting.Cfg, opts Options, apiOpts api.ServerOp
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService, moduleRegisterer, storageBackend)
|
||||
moduleServer, err := NewModule(opts, apiOpts, featureToggles, cfg, storageMetrics, bleveIndexMetrics, registerer, gatherer, tracingService, ossLicensingService, moduleRegisterer, storageBackend, hooksService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -6,8 +6,12 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/tracing"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
@@ -18,24 +22,38 @@ func (s *frontendService) contextMiddleware() web.Middleware {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
reqContext := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(ctx),
|
||||
Logger: log.New("context"),
|
||||
}
|
||||
span := trace.SpanFromContext(ctx)
|
||||
ctx = setRequestContext(ctx)
|
||||
|
||||
// inject ReqContext in the context
|
||||
ctx = context.WithValue(ctx, ctxkey.Key{}, reqContext)
|
||||
|
||||
// Set the context for the http.Request.Context
|
||||
// This modifies both r and reqContext.Req since they point to the same value
|
||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||
|
||||
traceID := tracing.TraceIDFromContext(ctx, false)
|
||||
if traceID != "" {
|
||||
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
||||
}
|
||||
// Preserve the original span so the setRequestContext span doesn't get propagated as a parent of the rest of the request
|
||||
ctx = trace.ContextWithSpan(ctx, span)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setRequestContext(ctx context.Context) context.Context {
|
||||
ctx, span := tracing.Start(ctx, "setRequestContext")
|
||||
defer span.End()
|
||||
|
||||
reqContext := &contextmodel.ReqContext{
|
||||
Context: web.FromContext(ctx),
|
||||
Logger: log.New("context"),
|
||||
SignedInUser: &user.SignedInUser{},
|
||||
}
|
||||
|
||||
// inject ReqContext in the context
|
||||
ctx = context.WithValue(ctx, ctxkey.Key{}, reqContext)
|
||||
|
||||
// Set the context for the http.Request.Context
|
||||
// This modifies both r and reqContext.Req since they point to the same value
|
||||
*reqContext.Req = *reqContext.Req.WithContext(ctx)
|
||||
|
||||
traceID := tracing.TraceIDFromContext(ctx, false)
|
||||
if traceID != "" {
|
||||
reqContext.Logger = reqContext.Logger.New("traceID", traceID)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/middleware/requestmeta"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
fswebassets "github.com/grafana/grafana/pkg/services/frontend/webassets"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -50,13 +51,13 @@ type frontendService struct {
|
||||
index *IndexProvider
|
||||
}
|
||||
|
||||
func ProvideFrontendService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, promGatherer prometheus.Gatherer, promRegister prometheus.Registerer, license licensing.Licensing) (*frontendService, error) {
|
||||
func ProvideFrontendService(cfg *setting.Cfg, features featuremgmt.FeatureToggles, promGatherer prometheus.Gatherer, promRegister prometheus.Registerer, license licensing.Licensing, hooksService *hooks.HooksService) (*frontendService, error) {
|
||||
assetsManifest, err := fswebassets.GetWebAssets(cfg, license)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
index, err := NewIndexProvider(cfg, assetsManifest)
|
||||
index, err := NewIndexProvider(cfg, assetsManifest, license, hooksService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,8 +11,11 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
@@ -24,6 +27,7 @@ func createTestService(t *testing.T, cfg *setting.Cfg) *frontendService {
|
||||
|
||||
features := featuremgmt.WithFeatures()
|
||||
license := &licensing.OSSLicensingService{}
|
||||
hooksService := hooks.ProvideService()
|
||||
|
||||
var promRegister prometheus.Registerer = prometheus.NewRegistry()
|
||||
promGatherer := promRegister.(*prometheus.Registry)
|
||||
@@ -32,7 +36,7 @@ func createTestService(t *testing.T, cfg *setting.Cfg) *frontendService {
|
||||
cfg.BuildVersion = "10.3.0"
|
||||
}
|
||||
|
||||
service, err := ProvideFrontendService(cfg, features, promGatherer, promRegister, license)
|
||||
service, err := ProvideFrontendService(cfg, features, promGatherer, promRegister, license, hooksService)
|
||||
require.NoError(t, err)
|
||||
|
||||
return service
|
||||
@@ -182,3 +186,56 @@ func TestFrontendService_Middleware(t *testing.T) {
|
||||
mux.ServeHTTP(recorder, req)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFrontendService_IndexHooks(t *testing.T) {
|
||||
publicDir := setupTestWebAssets(t)
|
||||
cfg := &setting.Cfg{
|
||||
HTTPPort: "3000",
|
||||
StaticRootPath: publicDir,
|
||||
BuildVersion: "10.3.0",
|
||||
}
|
||||
|
||||
t.Run("should handle hooks that modify buildInfo fields", func(t *testing.T) {
|
||||
service := createTestService(t, cfg)
|
||||
|
||||
// Add a hook that modifies various buildInfo fields
|
||||
service.index.hooksService.AddIndexDataHook(func(indexData *dtos.IndexViewData, req *contextmodel.ReqContext) {
|
||||
indexData.Settings.BuildInfo.Version = "99.99.99"
|
||||
indexData.Settings.BuildInfo.VersionString = "Custom Edition v99.99.99 (custom)"
|
||||
indexData.Settings.BuildInfo.Edition = "custom-edition"
|
||||
})
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
assert.Contains(t, body, "99.99.99", "Hook should have modified the version")
|
||||
assert.Contains(t, body, "Custom Edition v99.99.99 (custom)", "Hook should have modified the version string")
|
||||
assert.Contains(t, body, "custom-edition", "Hook should have modified the edition")
|
||||
})
|
||||
|
||||
t.Run("should work without any hooks registered", func(t *testing.T) {
|
||||
service := createTestService(t, cfg)
|
||||
|
||||
mux := web.New()
|
||||
service.addMiddlewares(mux)
|
||||
service.registerRoutes(mux)
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
recorder := httptest.NewRecorder()
|
||||
mux.ServeHTTP(recorder, req)
|
||||
|
||||
assert.Equal(t, 200, recorder.Code)
|
||||
body := recorder.Body.String()
|
||||
assert.Contains(t, body, "<div id=\"reactRoot\"></div>")
|
||||
// The build version comes from setting.BuildVersion (global), not cfg.BuildVersion
|
||||
// So we just check that the page renders successfully
|
||||
assert.Contains(t, body, "window.grafanaBootData")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package frontend
|
||||
|
||||
import "github.com/grafana/grafana/pkg/setting"
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
// This is a copy of dtos.FrontendSettingsDTO with only the fields that the frontend-service
|
||||
// sends, to prevent default values from overriding what comes from the /bootdata call.
|
||||
@@ -20,6 +23,8 @@ type FSFrontendSettings struct {
|
||||
PasswordHint string `json:"passwordHint,omitempty"`
|
||||
AnonymousEnabled bool `json:"anonymousEnabled,omitempty"`
|
||||
|
||||
BuildInfo dtos.FrontendSettingsBuildInfoDTO `json:"buildInfo"`
|
||||
|
||||
GoogleAnalyticsId string `json:"googleAnalyticsId,omitempty"`
|
||||
GoogleAnalytics4Id string `json:"googleAnalytics4Id,omitempty"`
|
||||
GoogleAnalytics4SendManualPageViews bool `json:"GoogleAnalytics4SendManualPageViews,omitempty"`
|
||||
|
||||
@@ -12,13 +12,18 @@ import (
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/licensing"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type IndexProvider struct {
|
||||
log logging.Logger
|
||||
index *template.Template
|
||||
data IndexViewData
|
||||
log logging.Logger
|
||||
index *template.Template
|
||||
data IndexViewData
|
||||
hooksService *hooks.HooksService
|
||||
}
|
||||
|
||||
type IndexViewData struct {
|
||||
@@ -51,12 +56,14 @@ var (
|
||||
htmlTemplates = template.Must(template.New("html").Delims("[[", "]]").ParseFS(templatesFS, `*.html`))
|
||||
)
|
||||
|
||||
func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets) (*IndexProvider, error) {
|
||||
func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets, license licensing.Licensing, hooksService *hooks.HooksService) (*IndexProvider, error) {
|
||||
t := htmlTemplates.Lookup("index.html")
|
||||
if t == nil {
|
||||
return nil, fmt.Errorf("missing index template")
|
||||
}
|
||||
|
||||
logger := logging.DefaultLogger.With("logger", "index-provider")
|
||||
|
||||
// subset of frontend settings needed for the login page
|
||||
// TODO what about enterprise settings here?
|
||||
frontendSettings := FSFrontendSettings{
|
||||
@@ -87,13 +94,13 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets) (*
|
||||
RudderstackWriteKey: cfg.RudderstackWriteKey,
|
||||
TrustedTypesDefaultPolicyEnabled: (cfg.CSPEnabled && strings.Contains(cfg.CSPTemplate, "require-trusted-types-for")) || (cfg.CSPReportOnlyEnabled && strings.Contains(cfg.CSPReportOnlyTemplate, "require-trusted-types-for")),
|
||||
VerifyEmailEnabled: cfg.VerifyEmailEnabled,
|
||||
BuildInfo: getBuildInfo(license, cfg),
|
||||
}
|
||||
|
||||
defaultUser := dtos.CurrentUser{}
|
||||
|
||||
return &IndexProvider{
|
||||
log: logging.DefaultLogger.With("logger", "index-provider"),
|
||||
index: t,
|
||||
log: logger,
|
||||
index: t,
|
||||
hooksService: hooksService,
|
||||
data: IndexViewData{
|
||||
AppTitle: "Grafana",
|
||||
AppSubUrl: cfg.AppSubURL, // Based on the request?
|
||||
@@ -109,13 +116,13 @@ func NewIndexProvider(cfg *setting.Cfg, assetsManifest dtos.EntryPointAssets) (*
|
||||
|
||||
Assets: assetsManifest,
|
||||
Settings: frontendSettings,
|
||||
DefaultUser: defaultUser,
|
||||
DefaultUser: dtos.CurrentUser{},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) {
|
||||
_, span := tracer.Start(request.Context(), "frontend.index.HandleRequest")
|
||||
ctx, span := tracer.Start(request.Context(), "frontend.index.HandleRequest")
|
||||
defer span.End()
|
||||
|
||||
if request.Method != "GET" {
|
||||
@@ -142,6 +149,9 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
||||
writer.Header().Set("Content-Security-Policy-Report-Only", policy)
|
||||
}
|
||||
|
||||
reqCtx := contexthandler.FromContext(ctx)
|
||||
p.runIndexDataHooks(reqCtx, &data)
|
||||
|
||||
writer.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
writer.WriteHeader(200)
|
||||
if err := p.index.Execute(writer, &data); err != nil {
|
||||
@@ -151,3 +161,43 @@ func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.
|
||||
panic(fmt.Sprintf("Error rendering index\n %s", err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IndexProvider) runIndexDataHooks(reqCtx *contextmodel.ReqContext, data *IndexViewData) {
|
||||
// Create a dummy struct to pass to the hooks, and then extract the data back out from it
|
||||
legacyIndexViewData := dtos.IndexViewData{
|
||||
Settings: &dtos.FrontendSettingsDTO{
|
||||
BuildInfo: data.Settings.BuildInfo,
|
||||
},
|
||||
}
|
||||
|
||||
p.hooksService.RunIndexDataHooks(&legacyIndexViewData, reqCtx)
|
||||
|
||||
data.Settings.BuildInfo = legacyIndexViewData.Settings.BuildInfo
|
||||
}
|
||||
|
||||
func getBuildInfo(license licensing.Licensing, cfg *setting.Cfg) dtos.FrontendSettingsBuildInfoDTO {
|
||||
version := setting.BuildVersion
|
||||
commit := setting.BuildCommit
|
||||
commitShort := getShortCommitHash(setting.BuildCommit, 10)
|
||||
buildstamp := setting.BuildStamp
|
||||
versionString := fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, version, commitShort)
|
||||
|
||||
buildInfo := dtos.FrontendSettingsBuildInfoDTO{
|
||||
Version: version,
|
||||
VersionString: versionString,
|
||||
Commit: commit,
|
||||
CommitShort: commitShort,
|
||||
Buildstamp: buildstamp,
|
||||
Edition: license.Edition(),
|
||||
Env: cfg.Env,
|
||||
}
|
||||
|
||||
return buildInfo
|
||||
}
|
||||
|
||||
func getShortCommitHash(commitHash string, maxLength int) string {
|
||||
if len(commitHash) > maxLength {
|
||||
return commitHash[:maxLength]
|
||||
}
|
||||
return commitHash
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user