Commit c04bc805 by Torkel Ödegaard Committed by GitHub

CDN: Adds support for serving assets over a CDN (#30691)

* CDN: Initial poc support for serving assets over a CDN

* Minor fix

* added build path and test

* fix lint error

* Added edition to cdn path

* Move master builds to a separate path

* Added error handling for the url parsing, changed setting name, and added docs

* Updated sample.ini

* Some property renames

* updated

* Minor update to html

* index template improvements

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update docs/sources/administration/configuration.md

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Added ContentDeliveryPrefix to Licence service

* updated docs

* Updated test mock

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
parent a8a3e026
...@@ -66,6 +66,9 @@ cert_key = ...@@ -66,6 +66,9 @@ cert_key =
# Unix socket path # Unix socket path
socket = /tmp/grafana.sock socket = /tmp/grafana.sock
# CDN Url
cdn_url =
#################################### Database ############################ #################################### Database ############################
[database] [database]
# You can configure the database connection by specifying type, host, name, user and password # You can configure the database connection by specifying type, host, name, user and password
......
...@@ -67,6 +67,9 @@ ...@@ -67,6 +67,9 @@
# Unix socket path # Unix socket path
;socket = ;socket =
# CDN Url
;cdn_url =
#################################### Database #################################### #################################### Database ####################################
[database] [database]
# You can configure the database connection by specifying type, host, name, user and password # You can configure the database connection by specifying type, host, name, user and password
......
...@@ -259,6 +259,15 @@ Path to the certificate key file (if `protocol` is set to `https` or `h2`). ...@@ -259,6 +259,15 @@ Path to the certificate key file (if `protocol` is set to `https` or `h2`).
Path where the socket should be created when `protocol=socket`. Make sure that Grafana has appropriate permissions before you change this setting. Path where the socket should be created when `protocol=socket`. Make sure that Grafana has appropriate permissions before you change this setting.
### cdn_url
> **Note**: Available in Grafana v7.4 and later versions.
Specify a full HTTP URL address to the root of your Grafana CDN assets. Grafana will add edition and version paths.
For example, given a cdn url like `https://cdn.myserver.com` grafana will try to load a javascript file from
`http://cdn.myserver.com/grafana-oss/v7.4.0/public/build/app.<hash>.js`.
<hr /> <hr />
## [database] ## [database]
......
...@@ -25,6 +25,7 @@ type IndexViewData struct { ...@@ -25,6 +25,7 @@ type IndexViewData struct {
AppleTouchIcon template.URL AppleTouchIcon template.URL
AppTitle string AppTitle string
Sentry *setting.Sentry Sentry *setting.Sentry
ContentDeliveryURL string
// Nonce is a cryptographic identifier for use with Content Security Policy. // Nonce is a cryptographic identifier for use with Content Security Policy.
Nonce string Nonce string
} }
......
...@@ -428,6 +428,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat ...@@ -428,6 +428,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
NavTree: navTree, NavTree: navTree,
Sentry: &hs.Cfg.Sentry, Sentry: &hs.Cfg.Sentry,
Nonce: c.RequestNonce, Nonce: c.RequestNonce,
ContentDeliveryURL: hs.Cfg.GetContentDeliveryURL((hs.License.Edition())),
} }
if setting.DisableGravatar { if setting.DisableGravatar {
......
...@@ -13,6 +13,9 @@ type Licensing interface { ...@@ -13,6 +13,9 @@ type Licensing interface {
// Return edition // Return edition
Edition() string Edition() string
// Used to build content delivery URL
ContentDeliveryPrefix() string
LicenseURL(user *SignedInUser) string LicenseURL(user *SignedInUser) string
StateInfo() string StateInfo() string
......
...@@ -403,6 +403,10 @@ func (t *testLicensingService) StateInfo() string { ...@@ -403,6 +403,10 @@ func (t *testLicensingService) StateInfo() string {
return "" return ""
} }
func (t *testLicensingService) ContentDeliveryPrefix() string {
return ""
}
func (t *testLicensingService) LicenseURL(user *models.SignedInUser) string { func (t *testLicensingService) LicenseURL(user *models.SignedInUser) string {
return "" return ""
} }
......
...@@ -32,6 +32,10 @@ func (*OSSLicensingService) StateInfo() string { ...@@ -32,6 +32,10 @@ func (*OSSLicensingService) StateInfo() string {
return "" return ""
} }
func (*OSSLicensingService) ContentDeliveryPrefix() string {
return "grafana-oss"
}
func (l *OSSLicensingService) LicenseURL(user *models.SignedInUser) string { func (l *OSSLicensingService) LicenseURL(user *models.SignedInUser) string {
if user.IsGrafanaAdmin { if user.IsGrafanaAdmin {
return l.Cfg.AppSubURL + "/admin/upgrading" return l.Cfg.AppSubURL + "/admin/upgrading"
......
...@@ -197,6 +197,7 @@ type Cfg struct { ...@@ -197,6 +197,7 @@ type Cfg struct {
SocketPath string SocketPath string
RouterLogging bool RouterLogging bool
Domain string Domain string
CDNRootURL *url.URL
// build // build
BuildVersion string BuildVersion string
...@@ -767,7 +768,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { ...@@ -767,7 +768,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "") provisioning := valueAsString(iniFile.Section("paths"), "provisioning", "")
cfg.ProvisioningPath = makeAbsolute(provisioning, HomePath) cfg.ProvisioningPath = makeAbsolute(provisioning, HomePath)
if err := readServerSettings(iniFile, cfg); err != nil { if err := cfg.readServerSettings(iniFile); err != nil {
return err return err
} }
...@@ -1251,7 +1252,7 @@ func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error { ...@@ -1251,7 +1252,7 @@ func readSnapshotsSettings(cfg *Cfg, iniFile *ini.File) error {
return nil return nil
} }
func readServerSettings(iniFile *ini.File, cfg *Cfg) error { func (cfg *Cfg) readServerSettings(iniFile *ini.File) error {
server := iniFile.Section("server") server := iniFile.Section("server")
var err error var err error
AppUrl, AppSubUrl, err = parseAppUrlAndSubUrl(server) AppUrl, AppSubUrl, err = parseAppUrlAndSubUrl(server)
...@@ -1263,8 +1264,8 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error { ...@@ -1263,8 +1264,8 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error {
cfg.AppURL = AppUrl cfg.AppURL = AppUrl
cfg.AppSubURL = AppSubUrl cfg.AppSubURL = AppSubUrl
cfg.ServeFromSubPath = ServeFromSubPath cfg.ServeFromSubPath = ServeFromSubPath
cfg.Protocol = HTTPScheme cfg.Protocol = HTTPScheme
protocolStr := valueAsString(server, "protocol", "http") protocolStr := valueAsString(server, "protocol", "http")
if protocolStr == "https" { if protocolStr == "https" {
...@@ -1297,9 +1298,34 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error { ...@@ -1297,9 +1298,34 @@ func readServerSettings(iniFile *ini.File, cfg *Cfg) error {
return err return err
} }
cdnURL := valueAsString(server, "cdn_url", "")
if cdnURL != "" {
cfg.CDNRootURL, err = url.Parse(cdnURL)
if err != nil {
return err
}
}
return nil return nil
} }
// GetContentDeliveryURL returns full content delivery URL with /<edition>/<version> added to URL
func (cfg *Cfg) GetContentDeliveryURL(prefix string) string {
if cfg.CDNRootURL != nil {
url := *cfg.CDNRootURL
preReleaseFolder := ""
if strings.Contains(cfg.BuildVersion, "pre") || strings.Contains(cfg.BuildVersion, "alpha") {
preReleaseFolder = "pre-releases"
}
url.Path = path.Join(url.Path, prefix, preReleaseFolder, cfg.BuildVersion)
return url.String()
}
return ""
}
func (cfg *Cfg) readDataSourcesSettings() { func (cfg *Cfg) readDataSourcesSettings() {
datasources := cfg.Raw.Section("datasources") datasources := cfg.Raw.Section("datasources")
cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000) cfg.DataSourceLimit = datasources.Key("datasource_limit").MustInt(5000)
......
...@@ -2,6 +2,7 @@ package setting ...@@ -2,6 +2,7 @@ package setting
import ( import (
"bufio" "bufio"
"net/url"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
...@@ -389,3 +390,35 @@ func TestAuthDurationSettings(t *testing.T) { ...@@ -389,3 +390,35 @@ func TestAuthDurationSettings(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime) require.Equal(t, maxLifetimeDurationTest, cfg.LoginMaxLifetime)
} }
func TestGetCDNPath(t *testing.T) {
var err error
cfg := NewCfg()
cfg.BuildVersion = "v7.5.0-11124"
cfg.CDNRootURL, err = url.Parse("http://cdn.grafana.com")
require.NoError(t, err)
require.Equal(t, "http://cdn.grafana.com/grafana-oss/v7.5.0-11124", cfg.GetContentDeliveryURL("grafana-oss"))
require.Equal(t, "http://cdn.grafana.com/grafana/v7.5.0-11124", cfg.GetContentDeliveryURL("grafana"))
}
func TestGetCDNPathWithPreReleaseVersionAndSubPath(t *testing.T) {
var err error
cfg := NewCfg()
cfg.BuildVersion = "v7.5.0-11124pre"
cfg.CDNRootURL, err = url.Parse("http://cdn.grafana.com/sub")
require.NoError(t, err)
require.Equal(t, "http://cdn.grafana.com/sub/grafana-oss/pre-releases/v7.5.0-11124pre", cfg.GetContentDeliveryURL("grafana-oss"))
require.Equal(t, "http://cdn.grafana.com/sub/grafana/pre-releases/v7.5.0-11124pre", cfg.GetContentDeliveryURL("grafana"))
}
// Adding a case for this in case we switch to proper semver version strings
func TestGetCDNPathWithAlphaVersion(t *testing.T) {
var err error
cfg := NewCfg()
cfg.BuildVersion = "v7.5.0-alpha.11124"
cfg.CDNRootURL, err = url.Parse("http://cdn.grafana.com")
require.NoError(t, err)
require.Equal(t, "http://cdn.grafana.com/grafana-oss/pre-releases/v7.5.0-alpha.11124", cfg.GetContentDeliveryURL("grafana-oss"))
require.Equal(t, "http://cdn.grafana.com/grafana/pre-releases/v7.5.0-alpha.11124", cfg.GetContentDeliveryURL("grafana"))
}
import app from './app'; declare let __webpack_public_path__: string;
/**
* Check if we are hosting files on cdn and set webpack public path
*/
if ((window as any).public_cdn_path) {
__webpack_public_path__ = (window as any).public_cdn_path;
}
import app from './app';
app.initEchoSrv(); app.initEchoSrv();
app.init(); app.init();
...@@ -3,16 +3,15 @@ ...@@ -3,16 +3,15 @@
<head> <head>
<script nonce="[[.Nonce]]"> <script nonce="[[.Nonce]]">
// https://github.com/GoogleChromeLabs/tti-polyfill // https://github.com/GoogleChromeLabs/tti-polyfill
!(function() { !(function () {
if ('PerformanceLongTaskTiming' in window) { if ('PerformanceLongTaskTiming' in window) {
var g = (window.__tti = { e: [] }); var g = (window.__tti = { e: [] });
g.o = new PerformanceObserver(function(l) { g.o = new PerformanceObserver(function (l) {
g.e = g.e.concat(l.getEntries()); g.e = g.e.concat(l.getEntries());
}); });
g.o.observe({ entryTypes: ['longtask'] }); g.o.observe({ entryTypes: ['longtask'] });
} }
})(); })();
</script> </script>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
...@@ -25,16 +24,15 @@ ...@@ -25,16 +24,15 @@
<link <link
rel="preload" rel="preload"
href="public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2" href="[[.ContentDeliveryURL]]/public/fonts/roboto/RxZJdnzeo3R5zSexge8UUVtXRa8TVwTICgirnJhmVJw.woff2"
as="font" as="font"
crossorigin crossorigin
/> />
<link rel="icon" type="image/png" href="[[.FavIcon]]" /> <link rel="icon" type="image/png" href="[[.FavIcon]]" />
<link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" /> <link rel="apple-touch-icon" sizes="180x180" href="[[.AppleTouchIcon]]" />
<link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28" /> <link rel="mask-icon" href="[[.ContentDeliveryURL]]/public/img/grafana_mask_icon.svg" color="#F05A28" />
<link rel="stylesheet" href="[[.ContentDeliveryURL]]/public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
<link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].<%= webpack.hash %>.css" />
<script nonce="[[.Nonce]]"> <script nonce="[[.Nonce]]">
performance.mark('css done blocking'); performance.mark('css done blocking');
...@@ -199,14 +197,17 @@ ...@@ -199,14 +197,17 @@
</p> </p>
<p> <p>
1. This could be caused by your reverse proxy settings.<br /><br /> 1. This could be caused by your reverse proxy settings.<br /><br />
2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not using a reverse proxy make sure to set serve_from_sub_path to true.<br /> 2. If you host grafana under subpath make sure your grafana.ini root_url setting includes subpath. If not
using a reverse proxy make sure to set serve_from_sub_path to true.<br />
<br /> <br />
3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn 3. If you have a local dev build make sure you build frontend using: yarn start, yarn start:hot, or yarn
build<br /> build<br />
<br /> <br />
4. Sometimes restarting grafana-server can help<br /> 4. Sometimes restarting grafana-server can help<br />
<br /> <br />
5. You might be using a non-supported browser. To make sure read the documentation on <a href="https://grafana.com/docs/grafana/latest/installation/requirements/#supported-web-browsers">supported browsers</a>. 5. Check if you are using a non-supported browser. For more information, refer to the list of <a
href="https://grafana.com/docs/grafana/latest/installation/requirements/#supported-web-browsers">
supported browsers</a>.
</p> </p>
</div> </div>
</div> </div>
...@@ -217,7 +218,7 @@ ...@@ -217,7 +218,7 @@
<search-wrapper></search-wrapper> <search-wrapper></search-wrapper>
<div class="main-view"> <div class="main-view">
<div ng-view class="scroll-canvas"></div> <div ng-view class="scroll-canvas"></div>
</div> </div>
</grafana-app> </grafana-app>
...@@ -235,57 +236,64 @@ ...@@ -235,57 +236,64 @@
preloader[0].className = "preloader preloader--done"; preloader[0].className = "preloader preloader--done";
} }
}; };
window.public_cdn_path = '[[.ContentDeliveryURL]]/public/build/';
</script> </script>
[[if .GoogleTagManagerId]] [[if .GoogleTagManagerId]]
<!-- Google Tag Manager --> <!-- Google Tag Manager -->
<script nonce="[[.Nonce]]"> <script nonce="[[.Nonce]]">
dataLayer = [ dataLayer = [
{ {
IsSignedIn: '[[.User.IsSignedIn]]', IsSignedIn: '[[.User.IsSignedIn]]',
Email: '[[.User.Email]]', Email: '[[.User.Email]]',
Name: '[[.User.Name]]', Name: '[[.User.Name]]',
UserId: '[[.User.Id]]', UserId: '[[.User.Id]]',
OrgId: '[[.User.OrgId]]', OrgId: '[[.User.OrgId]]',
OrgName: '[[.User.OrgName]]', OrgName: '[[.User.OrgName]]',
}, },
]; ];
</script> </script>
<noscript> <noscript>
<iframe <iframe
src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]" src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]"
height="0" height="0"
width="0" width="0"
style="display:none;visibility:hidden" style="display: none; visibility: hidden"
></iframe> ></iframe>
</noscript> </noscript>
<script nonce="[[.Nonce]]"> <script nonce="[[.Nonce]]">
(function(w, d, s, l, i) { (function (w, d, s, l, i) {
w[l] = w[l] || []; w[l] = w[l] || [];
w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
var f = d.getElementsByTagName(s)[0], var f = d.getElementsByTagName(s)[0],
j = d.createElement(s), j = d.createElement(s),
dl = l != 'dataLayer' ? '&l=' + l : ''; dl = l != 'dataLayer' ? '&l=' + l : '';
j.async = true; j.async = true;
j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
f.parentNode.insertBefore(j, f); f.parentNode.insertBefore(j, f);
})(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]'); })(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');
</script> </script>
<!-- End Google Tag Manager --> <!-- End Google Tag Manager -->
[[end]] [[end]]
<% <% for (key in htmlWebpackPlugin.files.chunks) { %>
for (key in htmlWebpackPlugin.files.chunks) { %><% <% if (htmlWebpackPlugin.files.jsIntegrity) { %>
if (htmlWebpackPlugin.files.jsIntegrity) { %> <script
<script nonce="[[.Nonce]]" nonce="[[.Nonce]]"
src="<%= htmlWebpackPlugin.files.chunks[key].entry %>" src="[[.ContentDeliveryURL]]/<%= htmlWebpackPlugin.files.chunks[key].entry %>"
type="text/javascript" type="text/javascript"
integrity="<%= htmlWebpackPlugin.files.jsIntegrity[htmlWebpackPlugin.files.js.indexOf(htmlWebpackPlugin.files.chunks[key].entry)] %>" integrity="<%= htmlWebpackPlugin.files.jsIntegrity[htmlWebpackPlugin.files.js.indexOf(htmlWebpackPlugin.files.chunks[key].entry)] %>"
crossorigin="<%= webpackConfig.output.crossOriginLoading %>"></script><% crossorigin="<%= webpackConfig.output.crossOriginLoading %>">
} else { %> </script>
<script nonce="[[.Nonce]]" src="<%= htmlWebpackPlugin.files.chunks[key].entry %>" type="text/javascript"></script><% <% } else { %>
} %><% <script
} %> nonce="[[.Nonce]]"
src="[[.ContentDeliveryURL]]/<%= htmlWebpackPlugin.files.chunks[key].entry %>"
type="text/javascript">
</script>
<% } %>
<% } %>
<script nonce="[[.Nonce]]"> <script nonce="[[.Nonce]]">
performance.mark('js done blocking'); performance.mark('js done blocking');
</script> </script>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment