mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 18:13:09 +08:00
FEMT: Add feature toggle and expose the service in regular grafana (#104428)
This commit is contained in:
@ -1014,6 +1014,10 @@ export interface FeatureToggles {
|
|||||||
*/
|
*/
|
||||||
pluginsAutoUpdate?: boolean;
|
pluginsAutoUpdate?: boolean;
|
||||||
/**
|
/**
|
||||||
|
* Register MT frontend
|
||||||
|
*/
|
||||||
|
multiTenantFrontend?: boolean;
|
||||||
|
/**
|
||||||
* Enables the alerting list view v2 preview toggle
|
* Enables the alerting list view v2 preview toggle
|
||||||
*/
|
*/
|
||||||
alertingListViewV2PreviewToggle?: boolean;
|
alertingListViewV2PreviewToggle?: boolean;
|
||||||
|
@ -48,6 +48,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/frontend"
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
|
||||||
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
publicdashboardsapi "github.com/grafana/grafana/pkg/services/publicdashboards/api"
|
||||||
@ -85,6 +86,14 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/login", hs.LoginView)
|
r.Get("/login", hs.LoginView)
|
||||||
r.Get("/invite/:code", hs.Index)
|
r.Get("/invite/:code", hs.Index)
|
||||||
|
|
||||||
|
if hs.Features.IsEnabledGlobally(featuremgmt.FlagMultiTenantFrontend) {
|
||||||
|
index, err := frontend.NewIndexProvider(hs.Cfg, hs.License)
|
||||||
|
if err != nil {
|
||||||
|
panic(err) // ???
|
||||||
|
}
|
||||||
|
r.Get("/mtfe", index.HandleRequest)
|
||||||
|
}
|
||||||
|
|
||||||
// authed views
|
// authed views
|
||||||
r.Get("/", reqSignedIn, hs.Index)
|
r.Get("/", reqSignedIn, hs.Index)
|
||||||
r.Get("/profile/", reqSignedInNoAnonymous, hs.Index)
|
r.Get("/profile/", reqSignedInNoAnonymous, hs.Index)
|
||||||
|
@ -31,7 +31,7 @@ func ContentSecurityPolicy(cfg *setting.Cfg, logger log.Logger) func(http.Handle
|
|||||||
func nonceMiddleware(next http.Handler, logger log.Logger) http.Handler {
|
func nonceMiddleware(next http.Handler, logger log.Logger) http.Handler {
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
ctx := contexthandler.FromContext(req.Context())
|
ctx := contexthandler.FromContext(req.Context())
|
||||||
nonce, err := generateNonce()
|
nonce, err := GenerateNonce()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("Failed to generate CSP nonce", "err", err)
|
logger.Error("Failed to generate CSP nonce", "err", err)
|
||||||
ctx.JsonApiErr(500, "Failed to generate CSP nonce", err)
|
ctx.JsonApiErr(500, "Failed to generate CSP nonce", err)
|
||||||
@ -68,7 +68,7 @@ func ReplacePolicyVariables(policyTemplate, appURL, nonce string) string {
|
|||||||
return policy
|
return policy
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateNonce() (string, error) {
|
func GenerateNonce() (string, error) {
|
||||||
var buf [16]byte
|
var buf [16]byte
|
||||||
if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil {
|
if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -9,6 +9,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
|
||||||
"github.com/grafana/dskit/services"
|
"github.com/grafana/dskit/services"
|
||||||
"github.com/grafana/grafana/pkg/api"
|
"github.com/grafana/grafana/pkg/api"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
@ -16,16 +18,24 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/authz"
|
"github.com/grafana/grafana/pkg/services/authz"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/frontend"
|
"github.com/grafana/grafana/pkg/services/frontend"
|
||||||
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
"github.com/grafana/grafana/pkg/storage/unified/resource"
|
||||||
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
"github.com/grafana/grafana/pkg/storage/unified/sql"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewModule returns an instance of a ModuleServer, responsible for managing
|
// NewModule returns an instance of a ModuleServer, responsible for managing
|
||||||
// dskit modules (services).
|
// dskit modules (services).
|
||||||
func NewModule(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, promGatherer prometheus.Gatherer) (*ModuleServer, error) {
|
func NewModule(opts Options,
|
||||||
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, promGatherer)
|
apiOpts api.ServerOptions,
|
||||||
|
features featuremgmt.FeatureToggles,
|
||||||
|
cfg *setting.Cfg,
|
||||||
|
storageMetrics *resource.StorageMetrics,
|
||||||
|
indexMetrics *resource.BleveIndexMetrics,
|
||||||
|
promGatherer prometheus.Gatherer,
|
||||||
|
license licensing.Licensing,
|
||||||
|
) (*ModuleServer, error) {
|
||||||
|
s, err := newModuleServer(opts, apiOpts, features, cfg, storageMetrics, indexMetrics, promGatherer, license)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -37,7 +47,7 @@ func NewModule(opts Options, apiOpts api.ServerOptions, features featuremgmt.Fea
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, promGatherer prometheus.Gatherer) (*ModuleServer, error) {
|
func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremgmt.FeatureToggles, cfg *setting.Cfg, storageMetrics *resource.StorageMetrics, indexMetrics *resource.BleveIndexMetrics, promGatherer prometheus.Gatherer, license licensing.Licensing) (*ModuleServer, error) {
|
||||||
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
rootCtx, shutdownFn := context.WithCancel(context.Background())
|
||||||
|
|
||||||
s := &ModuleServer{
|
s := &ModuleServer{
|
||||||
@ -56,6 +66,7 @@ func newModuleServer(opts Options, apiOpts api.ServerOptions, features featuremg
|
|||||||
storageMetrics: storageMetrics,
|
storageMetrics: storageMetrics,
|
||||||
indexMetrics: indexMetrics,
|
indexMetrics: indexMetrics,
|
||||||
promGatherer: promGatherer,
|
promGatherer: promGatherer,
|
||||||
|
license: license,
|
||||||
}
|
}
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
@ -79,6 +90,7 @@ type ModuleServer struct {
|
|||||||
mtx sync.Mutex
|
mtx sync.Mutex
|
||||||
storageMetrics *resource.StorageMetrics
|
storageMetrics *resource.StorageMetrics
|
||||||
indexMetrics *resource.BleveIndexMetrics
|
indexMetrics *resource.BleveIndexMetrics
|
||||||
|
license licensing.Licensing
|
||||||
|
|
||||||
pidFile string
|
pidFile string
|
||||||
version string
|
version string
|
||||||
@ -153,7 +165,7 @@ func (s *ModuleServer) Run() error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
m.RegisterModule(modules.FrontendServer, func() (services.Service, error) {
|
m.RegisterModule(modules.FrontendServer, func() (services.Service, error) {
|
||||||
return frontend.ProvideFrontendService(s.cfg, s.promGatherer)
|
return frontend.ProvideFrontendService(s.cfg, s.promGatherer, s.license)
|
||||||
})
|
})
|
||||||
|
|
||||||
m.RegisterModule(modules.All, nil)
|
m.RegisterModule(modules.All, nil)
|
||||||
|
@ -1738,13 +1738,19 @@ var (
|
|||||||
FrontendOnly: true,
|
FrontendOnly: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
||||||
Name: "pluginsAutoUpdate",
|
Name: "pluginsAutoUpdate",
|
||||||
Description: "Enables auto-updating of users installed plugins",
|
Description: "Enables auto-updating of users installed plugins",
|
||||||
Stage: FeatureStageExperimental,
|
Stage: FeatureStageExperimental,
|
||||||
FrontendOnly: false,
|
FrontendOnly: false,
|
||||||
Owner: grafanaPluginsPlatformSquad,
|
Owner: grafanaPluginsPlatformSquad,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "multiTenantFrontend",
|
||||||
|
Description: "Register MT frontend",
|
||||||
|
Stage: FeatureStageExperimental,
|
||||||
|
FrontendOnly: false,
|
||||||
|
Owner: grafanaFrontendPlatformSquad,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "alertingListViewV2PreviewToggle",
|
Name: "alertingListViewV2PreviewToggle",
|
||||||
Description: "Enables the alerting list view v2 preview toggle",
|
Description: "Enables the alerting list view v2 preview toggle",
|
||||||
|
@ -228,6 +228,7 @@ unifiedNavbars,GA,@grafana/plugins-platform-backend,false,false,true
|
|||||||
logsPanelControls,preview,@grafana/observability-logs,false,false,true
|
logsPanelControls,preview,@grafana/observability-logs,false,false,true
|
||||||
metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
metricsFromProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,true
|
||||||
pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
|
pluginsAutoUpdate,experimental,@grafana/plugins-platform-backend,false,false,false
|
||||||
|
multiTenantFrontend,experimental,@grafana/grafana-frontend-platform,false,false,false
|
||||||
alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true
|
alertingListViewV2PreviewToggle,privatePreview,@grafana/alerting-squad,false,false,true
|
||||||
alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false
|
alertRuleUseFiredAtForStartsAt,experimental,@grafana/alerting-squad,false,false,false
|
||||||
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
|
alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true
|
||||||
|
|
@ -923,6 +923,10 @@ const (
|
|||||||
// Enables auto-updating of users installed plugins
|
// Enables auto-updating of users installed plugins
|
||||||
FlagPluginsAutoUpdate = "pluginsAutoUpdate"
|
FlagPluginsAutoUpdate = "pluginsAutoUpdate"
|
||||||
|
|
||||||
|
// FlagMultiTenantFrontend
|
||||||
|
// Register MT frontend
|
||||||
|
FlagMultiTenantFrontend = "multiTenantFrontend"
|
||||||
|
|
||||||
// FlagAlertingListViewV2PreviewToggle
|
// FlagAlertingListViewV2PreviewToggle
|
||||||
// Enables the alerting list view v2 preview toggle
|
// Enables the alerting list view v2 preview toggle
|
||||||
FlagAlertingListViewV2PreviewToggle = "alertingListViewV2PreviewToggle"
|
FlagAlertingListViewV2PreviewToggle = "alertingListViewV2PreviewToggle"
|
||||||
|
@ -1976,6 +1976,18 @@
|
|||||||
"codeowner": "@grafana/alerting-squad"
|
"codeowner": "@grafana/alerting-squad"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "multiTenantFrontend",
|
||||||
|
"resourceVersion": "1745438197175",
|
||||||
|
"creationTimestamp": "2025-04-23T19:56:37Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Register MT frontend",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/grafana-frontend-platform"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "multiTenantTempCredentials",
|
"name": "multiTenantTempCredentials",
|
||||||
@ -1989,6 +2001,19 @@
|
|||||||
"hideFromDocs": true
|
"hideFromDocs": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"name": "multitenantFrontend",
|
||||||
|
"resourceVersion": "1745438122785",
|
||||||
|
"creationTimestamp": "2025-04-23T19:55:22Z",
|
||||||
|
"deletionTimestamp": "2025-04-23T19:56:37Z"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"description": "Register MT frontend",
|
||||||
|
"stage": "experimental",
|
||||||
|
"codeowner": "@grafana/grafana-frontend-platform"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"name": "mysqlAnsiQuotes",
|
"name": "mysqlAnsiQuotes",
|
||||||
|
@ -6,11 +6,13 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/dskit/services"
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
|
"github.com/grafana/dskit/services"
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
type frontendService struct {
|
type frontendService struct {
|
||||||
@ -20,13 +22,21 @@ type frontendService struct {
|
|||||||
log log.Logger
|
log log.Logger
|
||||||
errChan chan error
|
errChan chan error
|
||||||
promGatherer prometheus.Gatherer
|
promGatherer prometheus.Gatherer
|
||||||
|
|
||||||
|
index *IndexProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideFrontendService(cfg *setting.Cfg, promGatherer prometheus.Gatherer) (*frontendService, error) {
|
func ProvideFrontendService(cfg *setting.Cfg, promGatherer prometheus.Gatherer, license licensing.Licensing) (*frontendService, error) {
|
||||||
|
index, err := NewIndexProvider(cfg, license)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
s := &frontendService{
|
s := &frontendService{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
log: log.New("frontend-server"),
|
log: log.New("frontend-server"),
|
||||||
promGatherer: promGatherer,
|
promGatherer: promGatherer,
|
||||||
|
index: index,
|
||||||
}
|
}
|
||||||
s.BasicService = services.NewBasicService(s.start, s.running, s.stop)
|
s.BasicService = services.NewBasicService(s.start, s.running, s.stop)
|
||||||
return s, nil
|
return s, nil
|
||||||
@ -64,7 +74,7 @@ func (s *frontendService) newFrontendServer(ctx context.Context) *http.Server {
|
|||||||
|
|
||||||
router := http.NewServeMux()
|
router := http.NewServeMux()
|
||||||
router.Handle("/metrics", promhttp.HandlerFor(s.promGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true}))
|
router.Handle("/metrics", promhttp.HandlerFor(s.promGatherer, promhttp.HandlerOpts{EnableOpenMetrics: true}))
|
||||||
router.HandleFunc("/", s.handleRequest)
|
router.HandleFunc("/", s.index.HandleRequest)
|
||||||
|
|
||||||
server := &http.Server{
|
server := &http.Server{
|
||||||
// 5s timeout for header reads to avoid Slowloris attacks (https://thetooth.io/blog/slowloris-attack/)
|
// 5s timeout for header reads to avoid Slowloris attacks (https://thetooth.io/blog/slowloris-attack/)
|
||||||
@ -76,34 +86,3 @@ func (s *frontendService) newFrontendServer(ctx context.Context) *http.Server {
|
|||||||
|
|
||||||
return server
|
return server
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *frontendService) handleRequest(writer http.ResponseWriter, request *http.Request) {
|
|
||||||
// This should:
|
|
||||||
// - get correct asset urls from fs or cdn
|
|
||||||
// - generate a nonce
|
|
||||||
// - render them into the index.html
|
|
||||||
// - and return it to the user!
|
|
||||||
|
|
||||||
s.log.Info("handling request", "method", request.Method, "url", request.URL.String())
|
|
||||||
htmlContent := `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Grafana Frontend Server</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: sans-serif;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Grafana Frontend Server</h1>
|
|
||||||
<p>This is a simple static HTML page served by the Grafana frontend server module.</p>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
|
|
||||||
writer.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
_, err := writer.Write([]byte(htmlContent))
|
|
||||||
if err != nil {
|
|
||||||
s.log.Error("could not write to response", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
108
pkg/services/frontend/index.go
Normal file
108
pkg/services/frontend/index.go
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
package frontend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-app-sdk/logging"
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/api/webassets"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/services/licensing"
|
||||||
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexProvider struct {
|
||||||
|
log logging.Logger
|
||||||
|
index *template.Template
|
||||||
|
data IndexViewData
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexViewData struct {
|
||||||
|
CSPContent string
|
||||||
|
CSPEnabled bool
|
||||||
|
IsDevelopmentEnv bool
|
||||||
|
|
||||||
|
AppSubUrl string
|
||||||
|
BuildVersion string
|
||||||
|
BuildCommit string
|
||||||
|
AppTitle string
|
||||||
|
|
||||||
|
Assets *dtos.EntryPointAssets // Includes CDN info
|
||||||
|
|
||||||
|
// Nonce is a cryptographic identifier for use with Content Security Policy.
|
||||||
|
Nonce string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Templates setup.
|
||||||
|
var (
|
||||||
|
//go:embed *.html
|
||||||
|
templatesFS embed.FS
|
||||||
|
|
||||||
|
// templates
|
||||||
|
htmlTemplates = template.Must(template.New("html").Delims("[[", "]]").ParseFS(templatesFS, `*.html`))
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewIndexProvider(cfg *setting.Cfg, license licensing.Licensing) (*IndexProvider, error) {
|
||||||
|
assets, err := webassets.GetWebAssets(context.Background(), cfg, license)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
t := htmlTemplates.Lookup("index.html")
|
||||||
|
if t == nil {
|
||||||
|
return nil, fmt.Errorf("missing index template")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &IndexProvider{
|
||||||
|
log: logging.DefaultLogger.With("logger", "index-provider"),
|
||||||
|
index: t,
|
||||||
|
data: IndexViewData{
|
||||||
|
AppTitle: "Grafana",
|
||||||
|
AppSubUrl: cfg.AppSubURL, // Based on the request?
|
||||||
|
BuildVersion: cfg.BuildVersion,
|
||||||
|
BuildCommit: cfg.BuildCommit,
|
||||||
|
Assets: assets,
|
||||||
|
|
||||||
|
CSPEnabled: cfg.CSPEnabled,
|
||||||
|
CSPContent: cfg.CSPTemplate,
|
||||||
|
|
||||||
|
IsDevelopmentEnv: cfg.Env == setting.Dev,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *IndexProvider) HandleRequest(writer http.ResponseWriter, request *http.Request) {
|
||||||
|
if request.Method != "GET" {
|
||||||
|
writer.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nonce, err := middleware.GenerateNonce()
|
||||||
|
if err != nil {
|
||||||
|
p.log.Error("error creating nonce", "err", err)
|
||||||
|
writer.WriteHeader(500)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO -- restructure so the static stuff is under one variable and the rest is dynamic
|
||||||
|
data := p.data // copy everything
|
||||||
|
data.Nonce = nonce
|
||||||
|
|
||||||
|
if data.CSPEnabled {
|
||||||
|
data.CSPContent = middleware.ReplacePolicyVariables(p.data.CSPContent, p.data.AppSubUrl, data.Nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||||
|
writer.WriteHeader(200)
|
||||||
|
if err := p.index.Execute(writer, &data); err != nil {
|
||||||
|
if errors.Is(err, syscall.EPIPE) { // Client has stopped listening.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(fmt.Sprintf("Error rendering index\n %s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
33
pkg/services/frontend/index.html
Normal file
33
pkg/services/frontend/index.html
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
[[ if and .CSPEnabled .IsDevelopmentEnv ]]
|
||||||
|
<!-- Cypress overwrites CSP headers in HTTP requests, so this is required for e2e tests-->
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="[[.CSPContent]]"/>
|
||||||
|
[[ end ]]
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<meta name="viewport" content="width=device-width" />
|
||||||
|
<meta name="theme-color" content="#000" />
|
||||||
|
|
||||||
|
<title>[[.AppTitle]]</title>
|
||||||
|
|
||||||
|
<base href="[[.AppSubUrl]]/" />
|
||||||
|
|
||||||
|
<link rel="mask-icon" href="[[.Assets.ContentDeliveryURL]]public/img/grafana_mask_icon.svg" color="#F05A28" />
|
||||||
|
|
||||||
|
[[range $asset := .Assets.CSSFiles]]
|
||||||
|
<link rel="stylesheet" href="[[$asset.FilePath]]" />
|
||||||
|
[[end]]
|
||||||
|
|
||||||
|
<script nonce="[[.Nonce]]">
|
||||||
|
performance.mark('frontend_boot_css_time_seconds');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<h1>Grafana Frontend Server ([[.BuildVersion]])</h1>
|
||||||
|
<p>This is a simple static HTML page served by the Grafana frontend server module.</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Reference in New Issue
Block a user