Frontend: Foundations for multi tenant frontend (#78815)
* Frontend: Foundations for multi tenant frontend * improve manifest parsing for multi-tenant frontend (#78876) * add test * add test * ?? * Updates * Added cache * test cleanup * lint * fix test * fix error templates * cleanup * remove copy * revert changes to list testdata * comment cleanup * prepare integration tests * Remove integrety --------- Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
co-authored by
Ryan McKinley
parent
7b78061235
commit
ed128ea964
@@ -34,4 +34,16 @@ type IndexViewData struct {
|
||||
// Nonce is a cryptographic identifier for use with Content Security Policy.
|
||||
Nonce string
|
||||
NewsFeedEnabled bool
|
||||
Assets *EntryPointAssets
|
||||
}
|
||||
|
||||
type EntryPointAssets struct {
|
||||
JSFiles []EntryPointAsset
|
||||
CSSDark string
|
||||
CSSLight string
|
||||
}
|
||||
|
||||
type EntryPointAsset struct {
|
||||
FilePath string
|
||||
Integrity string
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/webassets"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/auth/identity"
|
||||
@@ -81,6 +82,10 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
}
|
||||
|
||||
theme := hs.getThemeForIndexData(prefs.Theme, c.Query("theme"))
|
||||
assets, err := webassets.GetWebAssets(hs.Cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOrgCount := 1
|
||||
userOrgs, err := hs.orgService.GetUserOrgList(c.Req.Context(), &org.GetUserOrgListQuery{UserID: userID})
|
||||
@@ -142,6 +147,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV
|
||||
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL(hs.License.ContentDeliveryPrefix()),
|
||||
LoadingLogo: "public/img/grafana_icon.svg",
|
||||
IsDevelopmentEnv: hs.Cfg.Env == setting.Dev,
|
||||
Assets: assets,
|
||||
}
|
||||
|
||||
if hs.Cfg.CSPEnabled {
|
||||
|
||||
@@ -52,6 +52,11 @@ func fakeSetIndexViewData(t *testing.T) {
|
||||
User: &dtos.CurrentUser{},
|
||||
Settings: &dtos.FrontendSettingsDTO{},
|
||||
NavTree: &navtree.NavTreeRoot{},
|
||||
Assets: &dtos.EntryPointAssets{
|
||||
JSFiles: []dtos.EntryPointAsset{},
|
||||
CSSDark: "dark.css",
|
||||
CSSLight: "light.css",
|
||||
},
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -63,7 +68,7 @@ func fakeViewIndex(t *testing.T) {
|
||||
getViewIndex = origGetViewIndex
|
||||
})
|
||||
getViewIndex = func() string {
|
||||
return "index-template"
|
||||
return "index"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1554
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,91 @@
|
||||
package webassets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
type ManifestInfo struct {
|
||||
FilePath string `json:"src,omitempty"`
|
||||
Integrity string `json:"integrity,omitempty"`
|
||||
|
||||
// The known entrypoints
|
||||
App *EntryPointInfo `json:"app,omitempty"`
|
||||
Dark *EntryPointInfo `json:"dark,omitempty"`
|
||||
Light *EntryPointInfo `json:"light,omitempty"`
|
||||
}
|
||||
|
||||
type EntryPointInfo struct {
|
||||
Assets struct {
|
||||
JS []string `json:"js,omitempty"`
|
||||
CSS []string `json:"css,omitempty"`
|
||||
} `json:"assets,omitempty"`
|
||||
}
|
||||
|
||||
var entryPointAssetsCache *dtos.EntryPointAssets = nil
|
||||
|
||||
func GetWebAssets(cfg *setting.Cfg) (*dtos.EntryPointAssets, error) {
|
||||
if cfg.Env != setting.Dev && entryPointAssetsCache != nil {
|
||||
return entryPointAssetsCache, nil
|
||||
}
|
||||
|
||||
result, err := readWebAssets(filepath.Join(cfg.StaticRootPath, "build", "assets-manifest.json"))
|
||||
entryPointAssetsCache = result
|
||||
|
||||
return entryPointAssetsCache, err
|
||||
}
|
||||
|
||||
func readWebAssets(manifestpath string) (*dtos.EntryPointAssets, error) {
|
||||
//nolint:gosec
|
||||
bytes, err := os.ReadFile(manifestpath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load assets-manifest.json %w", err)
|
||||
}
|
||||
|
||||
manifest := map[string]ManifestInfo{}
|
||||
err = json.Unmarshal(bytes, &manifest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read assets-manifest.json %w", err)
|
||||
}
|
||||
|
||||
integrity := make(map[string]string, 100)
|
||||
for _, v := range manifest {
|
||||
if v.Integrity != "" && v.FilePath != "" {
|
||||
integrity[v.FilePath] = v.Integrity
|
||||
}
|
||||
}
|
||||
|
||||
entryPoints, ok := manifest["entrypoints"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("could not find entrypoints in asssets-manifest")
|
||||
}
|
||||
|
||||
if entryPoints.App == nil || len(entryPoints.App.Assets.JS) == 0 {
|
||||
return nil, fmt.Errorf("missing app entry")
|
||||
}
|
||||
if entryPoints.Dark == nil || len(entryPoints.Dark.Assets.CSS) == 0 {
|
||||
return nil, fmt.Errorf("missing dark entry")
|
||||
}
|
||||
if entryPoints.Light == nil || len(entryPoints.Light.Assets.CSS) == 0 {
|
||||
return nil, fmt.Errorf("missing light entry")
|
||||
}
|
||||
|
||||
entryPointJSAssets := make([]dtos.EntryPointAsset, 0)
|
||||
for _, entry := range entryPoints.App.Assets.JS {
|
||||
entryPointJSAssets = append(entryPointJSAssets, dtos.EntryPointAsset{
|
||||
FilePath: entry,
|
||||
Integrity: integrity[entry],
|
||||
})
|
||||
}
|
||||
|
||||
return &dtos.EntryPointAssets{
|
||||
JSFiles: entryPointJSAssets,
|
||||
CSSDark: entryPoints.Dark.Assets.CSS[0],
|
||||
CSSLight: entryPoints.Light.Assets.CSS[0],
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package webassets
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestReadWebassets(t *testing.T) {
|
||||
assets, err := readWebAssets("testdata/sample-assets-manifest.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
dto, err := json.MarshalIndent(assets, "", " ")
|
||||
require.NoError(t, err)
|
||||
//fmt.Printf("%s\n", string(dto))
|
||||
|
||||
require.JSONEq(t, `{
|
||||
"JSFiles": [
|
||||
{
|
||||
"FilePath": "public/build/runtime.20ed8c01880b812ed29f.js",
|
||||
"Integrity": "sha256-rcdxIHk6cWgu4jiFa1a+pWlileYD/R72GaS8ZACBUdw= sha384-I/VJZQkt+TuJTvu61ihdWPds7EHfLrW5CxeQ0x9gtSqoPg9Z17Uawz1yoYaTdxqQ sha512-4CPAbh4KdTmGxHoQw4pgpYmgAquupVfwfo6UBV2cGU3vGFnEwkhq320037ETwWs+n9xB/bAMOvrdabp1SA1+8g=="
|
||||
},
|
||||
{
|
||||
"FilePath": "public/build/3951.4e474348841d792ab1ba.js",
|
||||
"Integrity": "sha256-dHqXXTRA3osYhHr9rol8hOV0nC4VP0pr5tbMp5VD95Q= sha384-4QJaSTibnxdYeYsLnmXtd1+If6IkAmXlLR0uYHN5+N+fS0FegHRH7MIFaRGjiO1B sha512-vRLEeEGbxBCx0z+l/m14fSK49reqWGA9zQzsCrD+TQQBmP07YIoRPwopMMyxtKljbbRFV0bW2bUZ7ZvzOZYoIQ=="
|
||||
},
|
||||
{
|
||||
"FilePath": "public/build/3651.4e8f7603e9778e1e9b59.js",
|
||||
"Integrity": "sha256-+N7caL91pVANd7C/aquAneRTjBQenCwaEKqj+3qkjxc= sha384-GQR7GyHPEwwEVph9gGYWEWvMYxkITwcOjieehbPidXZrybuQyw9cpDkjnWo1tj/w sha512-zyPM+8AxyLuECEXjb9w6Z2Sy8zmJdkfTWQphcvAb8AU4ZdkCqLmyjmOs/QQlpfKDe0wdOLyR3V9QgTDDlxtVlQ=="
|
||||
},
|
||||
{
|
||||
"FilePath": "public/build/1272.8c79fc44bf7cd993c953.js",
|
||||
"Integrity": "sha256-d7MRVimV83v4YQ5rdURfTaaFtiedXP3EMLT06gvvBuQ= sha384-8tRpYHQ+sEkZ8ptiIbKAbKPpHTJVnmaWDN56vJoWWUCzV1Q2w034wcJNKDJDJdAs sha512-cIZWoJHusF8qODBOj2j4b18ewcLLMo/92YQSwYQjln2G5e3o1bSO476ox2I2iecJ/tnhQK5j01h9BzTt3dNTrA=="
|
||||
},
|
||||
{
|
||||
"FilePath": "public/build/6902.070074e8f5a989b8f4c3.js",
|
||||
"Integrity": "sha256-TMo/uTZueyEHtkBzlLZzhwYKWF0epE4qbouo5xcwZkU= sha384-xylZJMtJ7+EsUBBdQZvPh+BeHJ3BnfclqI2vx/8QC9jvfYe/lhRsWW9OMJsxE/Aq sha512-EOmf+KZQMFPoTWAROL8bBLFfHhgvDH8ONycq37JaV7lz+sQOTaWBN2ZD0F/mMdOD5zueTg/Y1RAUP6apoEcHNQ=="
|
||||
},
|
||||
{
|
||||
"FilePath": "public/build/app.0439db6f56ee4aa501b2.js",
|
||||
"Integrity": "sha256-q6muaKY7BuN2Ff+00aw69628MXatcFnLNzWRnAD98DI= sha384-gv6lAbkngOHR05bvyOR8dm/J3wIjQQWSjyxK7W8vt2rG9uxcjvvDQV7aI6YbUhfX sha512-o/0mSlJ/OoqrpGdOIWCE3ZCe8n+qqLbgNCERtx9G8FIzsv++CvIWSGbbILjOTGfnEfEQWcKMH0macVpVBSe1Og=="
|
||||
}
|
||||
],
|
||||
"CSSDark": "public/build/grafana.dark.a28b24b45b2bbcc628cc.css",
|
||||
"CSSLight": "public/build/grafana.light.3572f6d5f8b7daa8d8d0.css"
|
||||
}`, string(dto))
|
||||
}
|
||||
@@ -149,9 +149,14 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
User: &dtos.CurrentUser{},
|
||||
Settings: &dtos.FrontendSettingsDTO{},
|
||||
NavTree: &navtree.NavTreeRoot{},
|
||||
Assets: &dtos.EntryPointAssets{
|
||||
JSFiles: []dtos.EntryPointAsset{},
|
||||
CSSDark: "dark.css",
|
||||
CSSLight: "light.css",
|
||||
},
|
||||
}
|
||||
t.Log("Calling HTML", "data", data)
|
||||
c.HTML(http.StatusOK, "index-template", data)
|
||||
c.HTML(http.StatusOK, "index", data)
|
||||
t.Log("Returned HTML with code 200")
|
||||
}
|
||||
sc.fakeReq("GET", "/").exec()
|
||||
@@ -199,7 +204,7 @@ func middlewareScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...func(
|
||||
cfg.LoginCookieName = "grafana_session"
|
||||
cfg.LoginMaxLifetime = loginMaxLifetime
|
||||
// Required when rendering errors
|
||||
cfg.ErrTemplateName = "error-template"
|
||||
cfg.ErrTemplateName = "error"
|
||||
for _, cb := range cbs {
|
||||
cb(cfg)
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/webassets"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/contexthandler"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@@ -135,13 +137,19 @@ func Recovery(cfg *setting.Cfg) web.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
assets, _ := webassets.GetWebAssets(cfg)
|
||||
if assets == nil {
|
||||
assets = &dtos.EntryPointAssets{JSFiles: []dtos.EntryPointAsset{}}
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Title string
|
||||
AppTitle string
|
||||
AppSubUrl string
|
||||
Theme string
|
||||
ErrorMsg string
|
||||
}{"Server Error", "Grafana", cfg.AppSubURL, cfg.DefaultTheme, ""}
|
||||
Assets *dtos.EntryPointAssets
|
||||
}{"Server Error", "Grafana", cfg.AppSubURL, cfg.DefaultTheme, "", assets}
|
||||
|
||||
if setting.Env == setting.Dev {
|
||||
if err, ok := r.(error); ok {
|
||||
|
||||
@@ -50,7 +50,7 @@ func panicHandler(c *contextmodel.ReqContext) {
|
||||
func recoveryScenario(t *testing.T, desc string, url string, fn scenarioFunc) {
|
||||
t.Run(desc, func(t *testing.T) {
|
||||
cfg := setting.NewCfg()
|
||||
cfg.ErrTemplateName = "error-template"
|
||||
cfg.ErrTemplateName = "error"
|
||||
cfg.UserFacingDefaultError = "test error"
|
||||
sc := &scenarioContext{
|
||||
t: t,
|
||||
|
||||
@@ -154,17 +154,41 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
||||
publicDir := filepath.Join(tmpDir, "public")
|
||||
err = os.MkdirAll(publicDir, 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
viewsDir := filepath.Join(publicDir, "views")
|
||||
err = fs.CopyRecursive(filepath.Join(rootDir, "public", "views"), viewsDir)
|
||||
require.NoError(t, err)
|
||||
// Copy index template to index.html, since Grafana will try to use the latter
|
||||
err = fs.CopyFile(filepath.Join(rootDir, "public", "views", "index-template.html"),
|
||||
filepath.Join(viewsDir, "index.html"))
|
||||
|
||||
// add a stub manifest to the build directory
|
||||
buildDir := filepath.Join(publicDir, "build")
|
||||
err = os.MkdirAll(buildDir, 0750)
|
||||
require.NoError(t, err)
|
||||
// Copy error template to error.html, since Grafana will try to use the latter
|
||||
err = fs.CopyFile(filepath.Join(rootDir, "public", "views", "error-template.html"),
|
||||
filepath.Join(viewsDir, "error.html"))
|
||||
err = os.WriteFile(filepath.Join(buildDir, "assets-manifest.json"), []byte(`{
|
||||
"entrypoints": {
|
||||
"app": {
|
||||
"assets": {
|
||||
"js": ["public/build/runtime.XYZ.js"]
|
||||
}
|
||||
},
|
||||
"dark": {
|
||||
"assets": {
|
||||
"css": ["public/build/dark.css"]
|
||||
}
|
||||
},
|
||||
"light": {
|
||||
"assets": {
|
||||
"css": ["public/build/light.css"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"runtime.50398398ecdeaf58968c.js": {
|
||||
"src": "public/build/runtime.XYZ.js",
|
||||
"integrity": "sha256-k1g7TksMHFQhhQGE"
|
||||
}
|
||||
}
|
||||
`), 0750)
|
||||
require.NoError(t, err)
|
||||
|
||||
emailsDir := filepath.Join(publicDir, "emails")
|
||||
err = fs.CopyRecursive(filepath.Join(rootDir, "public", "emails"), emailsDir)
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user