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:
Torkel Ödegaard
2023-12-05 08:34:22 +01:00
committed by GitHub
co-authored by Ryan McKinley
parent 7b78061235
commit ed128ea964
21 changed files with 1861 additions and 124 deletions
+12
View File
@@ -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
}
+6
View File
@@ -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 {
+6 -1
View File
@@ -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"
}
}
File diff suppressed because it is too large Load Diff
+91
View File
@@ -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
}
+48
View File
@@ -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))
}
+7 -2
View File
@@ -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)
}
+9 -1
View File
@@ -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 {
+1 -1
View File
@@ -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,
+30 -6
View File
@@ -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)