Fix -ui-content-path without regex (#9569)

* Add templating to inject JSON into an application/json script tag

Plus an external script in order to pick it out and inject the values we
need injecting into ember's environment meta tag.

The UI still uses env style naming (CONSUL_*) but we uses the new style
JSON/golang props behind the scenes.

Co-authored-by: Paul Banks <banks@banksco.de>
This commit is contained in:
John Cowen 2021-01-20 18:40:46 +00:00 committed by GitHub
parent 921c2a2bd8
commit d3ecb6d7a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 281 additions and 219 deletions

3
.changelog/9569.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
ui: Fixes an issue with setting -ui-content-path flag/config
```

File diff suppressed because one or more lines are too long

View File

@ -6,10 +6,8 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"os" "os"
"path" "path"
"regexp"
"strings" "strings"
"sync/atomic" "sync/atomic"
"text/template" "text/template"
@ -244,54 +242,13 @@ func (h *Handler) renderIndex(cfg *config.RuntimeConfig, fs http.FileSystem) ([]
} }
} }
// Sadly we can't perform all the replacements we need with Go template
// because some of them end up being rendered into an escaped json encoded
// meta tag by Ember build which messes up the Go template tags. After a few
// iterations of grossness, this seemed like the least bad for now. note we
// have to match the encoded double quotes around the JSON string value that
// is there as a placeholder so the end result is an actual JSON bool not a
// string containing "false" etc.
re := regexp.MustCompile(`%22__RUNTIME_(BOOL|STRING)_([A-Za-z0-9-_]+)__%22`)
content = []byte(re.ReplaceAllStringFunc(string(content), func(str string) string {
// Trim the prefix and suffix
pair := strings.TrimSuffix(strings.TrimPrefix(str, "%22__RUNTIME_"), "__%22")
parts := strings.SplitN(pair, "_", 2)
switch parts[0] {
case "BOOL":
if v, ok := tplData[parts[1]].(bool); ok && v {
return "true"
}
return "false"
case "STRING":
if v, ok := tplData[parts[1]].(string); ok {
if bs, err := json.Marshal(v); err == nil {
return url.PathEscape(string(bs))
}
// Error!
h.logger.Error("Encoding JSON value for UI template failed",
"placeholder", str,
"value", v,
)
// Fall through to return the empty string to make JSON parse
}
return `""` // Empty JSON string
}
// Unknown type is likely an error
h.logger.Error("Unknown placeholder type in UI template",
"placeholder", str,
)
// Return a literal empty string so the JSON still parses
return `""`
}))
tpl, err := template.New("index").Funcs(template.FuncMap{ tpl, err := template.New("index").Funcs(template.FuncMap{
"jsonEncodeAndEscape": func(data map[string]interface{}) (string, error) { "jsonEncode": func(data map[string]interface{}) (string, error) {
bs, err := json.Marshal(data) bs, err := json.MarshalIndent(data, "", " ")
if err != nil { if err != nil {
return "", fmt.Errorf("failed jsonEncodeAndEscape: %w", err) return "", fmt.Errorf("failed jsonEncode: %w", err)
} }
return url.PathEscape(string(bs)), nil return string(bs), nil
}, },
}).Parse(string(content)) }).Parse(string(content))
if err != nil { if err != nil {

View File

@ -1,16 +1,18 @@
package uiserver package uiserver
import ( import (
"encoding/json" "bytes"
"io"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "strings"
"testing" "testing"
"golang.org/x/net/html"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -34,14 +36,16 @@ func TestUIServerIndex(t *testing.T) {
path: "/", // Note /index.html redirects to / path: "/", // Note /index.html redirects to /
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantContains: []string{"<!-- CONSUL_VERSION:"}, wantContains: []string{"<!-- CONSUL_VERSION:"},
wantNotContains: []string{ wantUICfgJSON: `{
"__RUNTIME_BOOL_", "ACLsEnabled": false,
"__RUNTIME_STRING_", "LocalDatacenter": "dc1",
}, "ContentPath": "/ui/",
wantEnv: map[string]interface{}{ "UIConfig": {
"CONSUL_ACLS_ENABLED": false, "metrics_provider": "",
"CONSUL_DATACENTER_LOCAL": "dc1", "metrics_proxy_enabled": false,
}, "dashboard_url_templates": null
}
}`,
}, },
{ {
// We do this redirect just for UI dir since the app is a single page app // We do this redirect just for UI dir since the app is a single page app
@ -64,24 +68,18 @@ func TestUIServerIndex(t *testing.T) {
wantContains: []string{ wantContains: []string{
"<!-- CONSUL_VERSION:", "<!-- CONSUL_VERSION:",
}, },
wantNotContains: []string{
// This is a quick check to be sure that we actually URL encoded the
// JSON ui settings too. The assertions below could pass just fine even
// if we got that wrong because the decode would be a no-op if it wasn't
// URL encoded. But this just ensures that we don't see the raw values
// in the output because the quotes should be encoded.
`"a-very-unlikely-string"`,
},
wantEnv: map[string]interface{}{
"CONSUL_ACLS_ENABLED": false,
},
wantUICfgJSON: `{ wantUICfgJSON: `{
"ACLsEnabled": false,
"LocalDatacenter": "dc1",
"ContentPath": "/ui/",
"UIConfig": {
"metrics_provider": "foo", "metrics_provider": "foo",
"metrics_provider_options": { "metrics_provider_options": {
"a-very-unlikely-string":1 "a-very-unlikely-string":1
}, },
"metrics_proxy_enabled": false, "metrics_proxy_enabled": false,
"dashboard_url_templates": null "dashboard_url_templates": null
}
}`, }`,
}, },
{ {
@ -90,9 +88,16 @@ func TestUIServerIndex(t *testing.T) {
path: "/", path: "/",
wantStatus: http.StatusOK, wantStatus: http.StatusOK,
wantContains: []string{"<!-- CONSUL_VERSION:"}, wantContains: []string{"<!-- CONSUL_VERSION:"},
wantEnv: map[string]interface{}{ wantUICfgJSON: `{
"CONSUL_ACLS_ENABLED": true, "ACLsEnabled": true,
}, "LocalDatacenter": "dc1",
"ContentPath": "/ui/",
"UIConfig": {
"metrics_provider": "",
"metrics_proxy_enabled": false,
"dashboard_url_templates": null
}
}`,
}, },
{ {
name: "external transformation", name: "external transformation",
@ -110,13 +115,16 @@ func TestUIServerIndex(t *testing.T) {
wantContains: []string{ wantContains: []string{
"<!-- CONSUL_VERSION:", "<!-- CONSUL_VERSION:",
}, },
wantEnv: map[string]interface{}{
"CONSUL_SSO_ENABLED": true,
},
wantUICfgJSON: `{ wantUICfgJSON: `{
"ACLsEnabled": false,
"SSOEnabled": true,
"LocalDatacenter": "dc1",
"ContentPath": "/ui/",
"UIConfig": {
"metrics_provider": "bar", "metrics_provider": "bar",
"metrics_proxy_enabled": false, "metrics_proxy_enabled": false,
"dashboard_url_templates": null "dashboard_url_templates": null
}
}`, }`,
}, },
{ {
@ -148,13 +156,6 @@ func TestUIServerIndex(t *testing.T) {
for _, want := range tc.wantContains { for _, want := range tc.wantContains {
require.Contains(t, rec.Body.String(), want) require.Contains(t, rec.Body.String(), want)
} }
for _, wantNot := range tc.wantNotContains {
require.NotContains(t, rec.Body.String(), wantNot)
}
env := extractEnv(t, rec.Body.String())
for k, v := range tc.wantEnv {
require.Equal(t, v, env[k])
}
if tc.wantUICfgJSON != "" { if tc.wantUICfgJSON != "" {
require.JSONEq(t, tc.wantUICfgJSON, extractUIConfig(t, rec.Body.String())) require.JSONEq(t, tc.wantUICfgJSON, extractUIConfig(t, rec.Body.String()))
} }
@ -162,41 +163,48 @@ func TestUIServerIndex(t *testing.T) {
} }
} }
func extractMetaJSON(t *testing.T, name, content string) string { func extractApplicationJSON(t *testing.T, attrName, content string) string {
t.Helper() t.Helper()
// Find and extract the env meta tag. Why yes I _am_ using regexp to parse var scriptContent *html.Node
// HTML thanks for asking. In this case it's HTML with a very limited format var find func(node *html.Node)
// so I don't feel too bad but maybe I should.
// https://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags/1732454#1732454
re := regexp.MustCompile(`<meta name="` + name + `+" content="([^"]*)"`)
matches := re.FindStringSubmatch(content) // Recurse down the tree and pick out <script attrName=ourAttrName>
require.Len(t, matches, 2, "didn't find the %s meta tag", name) find = func(node *html.Node) {
if node.Type == html.ElementNode && node.Data == "script" {
// Unescape the JSON for i := 0; i < len(node.Attr); i++ {
jsonStr, err := url.PathUnescape(matches[1]) attr := node.Attr[i]
require.NoError(t, err) if attr.Key == attrName {
// find the script and save off the content, which in this case is
return jsonStr // the JSON we are looking for, once we have it finish up
scriptContent = node.FirstChild
return
}
}
}
for child := node.FirstChild; child != nil; child = child.NextSibling {
find(child)
}
} }
func extractEnv(t *testing.T, content string) map[string]interface{} { doc, err := html.Parse(strings.NewReader(content))
t.Helper()
js := extractMetaJSON(t, "consul-ui/config/environment", content)
var env map[string]interface{}
err := json.Unmarshal([]byte(js), &env)
require.NoError(t, err) require.NoError(t, err)
return env find(doc)
var buf bytes.Buffer
w := io.Writer(&buf)
renderErr := html.Render(w, scriptContent)
require.NoError(t, renderErr)
jsonStr := html.UnescapeString(buf.String())
return jsonStr
} }
func extractUIConfig(t *testing.T, content string) string { func extractUIConfig(t *testing.T, content string) string {
t.Helper() t.Helper()
return extractMetaJSON(t, "consul-ui/ui_config", content) return extractApplicationJSON(t, "data-consul-ui-config", content)
} }
type cfgFunc func(cfg *config.RuntimeConfig) type cfgFunc func(cfg *config.RuntimeConfig)

View File

@ -43,7 +43,10 @@ export default function(config = {}, win = window, doc = document) {
return {}; return {};
} }
}; };
const ui_config = JSON.parse(unescape(doc.getElementsByName('consul-ui/ui_config')[0].content)); const operatorConfig = JSON.parse(
doc.querySelector(`[data-${config.modulePrefix}-config]`).textContent
);
const ui_config = operatorConfig.UIConfig || {};
const scripts = doc.getElementsByTagName('script'); const scripts = doc.getElementsByTagName('script');
// we use the currently executing script as a reference // we use the currently executing script as a reference
// to figure out where we are for other things such as // to figure out where we are for other things such as
@ -57,6 +60,18 @@ export default function(config = {}, win = window, doc = document) {
const operator = function(str, env) { const operator = function(str, env) {
let protocol, dashboards, provider, proxy; let protocol, dashboards, provider, proxy;
switch (str) { switch (str) {
case 'CONSUL_NSPACES_ENABLED':
return typeof operatorConfig.NamespacesEnabled === 'undefined'
? false
: operatorConfig.NamespacesEnabled;
case 'CONSUL_SSO_ENABLED':
return typeof operatorConfig.SSOEnabled === 'undefined' ? false : operatorConfig.SSOEnabled;
case 'CONSUL_ACLS_ENABLED':
return typeof operatorConfig.ACLsEnabled === 'undefined'
? false
: operatorConfig.ACLsEnabled;
case 'CONSUL_DATACENTER_LOCAL':
return operatorConfig.LocalDatacenter;
case 'CONSUL_UI_CONFIG': case 'CONSUL_UI_CONFIG':
dashboards = {}; dashboards = {};
provider = env('CONSUL_METRICS_PROVIDER'); provider = env('CONSUL_METRICS_PROVIDER');
@ -155,6 +170,10 @@ export default function(config = {}, win = window, doc = document) {
// these are strings // these are strings
return user(str) || ui(str); return user(str) || ui(str);
case 'CONSUL_UI_CONFIG': case 'CONSUL_UI_CONFIG':
case 'CONSUL_DATACENTER_LOCAL':
case 'CONSUL_ACLS_ENABLED':
case 'CONSUL_NSPACES_ENABLED':
case 'CONSUL_SSO_ENABLED':
case 'CONSUL_METRICS_PROVIDER': case 'CONSUL_METRICS_PROVIDER':
case 'CONSUL_METRICS_PROXY_ENABLE': case 'CONSUL_METRICS_PROXY_ENABLE':
case 'CONSUL_SERVICE_DASHBOARD_URL': case 'CONSUL_SERVICE_DASHBOARD_URL':

View File

@ -92,11 +92,13 @@ module.exports = function(environment, $ = process.env) {
CONSUL_UI_DISABLE_ANCHOR_SELECTION: env('CONSUL_UI_DISABLE_ANCHOR_SELECTION', false), CONSUL_UI_DISABLE_ANCHOR_SELECTION: env('CONSUL_UI_DISABLE_ANCHOR_SELECTION', false),
// The following variables are runtime variables that are overwritten when // The following variables are runtime variables that are overwritten when
// the go binary services the index.html page // the go binary serves the index.html page
CONSUL_ACLS_ENABLED: false, operatorConfig: {
CONSUL_NSPACES_ENABLED: false, ACLsEnabled: false,
CONSUL_SSO_ENABLED: false, NamespacesEnabled: false,
CONSUL_DATACENTER_LOCAL: env('CONSUL_DATACENTER_LOCAL', 'dc1'), SSOEnabled: false,
LocalDatacenter: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
},
// Static variables used in multiple places throughout the UI // Static variables used in multiple places throughout the UI
CONSUL_HOME_URL: 'https://www.consul.io', CONSUL_HOME_URL: 'https://www.consul.io',
@ -112,9 +114,12 @@ module.exports = function(environment, $ = process.env) {
locationType: 'none', locationType: 'none',
// During testing ACLs default to being turned on // During testing ACLs default to being turned on
CONSUL_ACLS_ENABLED: env('CONSUL_ACLS_ENABLED', true), operatorConfig: {
CONSUL_NSPACES_ENABLED: env('CONSUL_NSPACES_ENABLED', false), ACLsEnabled: env('CONSUL_ACLS_ENABLED', true),
CONSUL_SSO_ENABLED: env('CONSUL_SSO_ENABLED', false), NamespacesEnabled: env('CONSUL_NSPACES_ENABLED', false),
SSOEnabled: env('CONSUL_SSO_ENABLED', false),
LocalDatacenter: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
},
'@hashicorp/ember-cli-api-double': { '@hashicorp/ember-cli-api-double': {
'auto-import': false, 'auto-import': false,
@ -143,9 +148,12 @@ module.exports = function(environment, $ = process.env) {
// different staging sites can be built with certain features disabled // different staging sites can be built with certain features disabled
// by setting an environment variable to 0 during building (e.g. // by setting an environment variable to 0 during building (e.g.
// CONSUL_NSPACES_ENABLED=0 make build) // CONSUL_NSPACES_ENABLED=0 make build)
CONSUL_ACLS_ENABLED: env('CONSUL_ACLS_ENABLED', true), operatorConfig: {
CONSUL_NSPACES_ENABLED: env('CONSUL_NSPACES_ENABLED', true), ACLsEnabled: env('CONSUL_ACLS_ENABLED', true),
CONSUL_SSO_ENABLED: env('CONSUL_SSO_ENABLED', true), NamespacesEnabled: env('CONSUL_NSPACES_ENABLED', true),
SSOEnabled: env('CONSUL_SSO_ENABLED', true),
LocalDatacenter: env('CONSUL_DATACENTER_LOCAL', 'dc1'),
},
'@hashicorp/ember-cli-api-double': { '@hashicorp/ember-cli-api-double': {
enabled: true, enabled: true,
@ -157,22 +165,9 @@ module.exports = function(environment, $ = process.env) {
break; break;
case environment === 'production': case environment === 'production':
ENV = Object.assign({}, ENV, { ENV = Object.assign({}, ENV, {
// These values are placeholders that are replaced when Consul renders // in production operatorConfig is populated at consul runtime from
// the index.html based on runtime config. They can't use Go template // operator configuration
// syntax since this object ends up JSON and URLencoded in an HTML meta operatorConfig: {},
// tag which obscured the Go template tag syntax.
//
// __RUNTIME_BOOL_Xxxx__ will be replaced with either "true" or "false"
// depending on whether the named variable is true or false in the data
// returned from `uiTemplateDataFromConfig`.
//
// __RUNTIME_STRING_Xxxx__ will be replaced with the literal string in
// the named variable in the data returned from
// `uiTemplateDataFromConfig`. It may be empty.
CONSUL_ACLS_ENABLED: '__RUNTIME_BOOL_ACLsEnabled__',
CONSUL_SSO_ENABLED: '__RUNTIME_BOOL_SSOEnabled__',
CONSUL_NSPACES_ENABLED: '__RUNTIME_BOOL_NamespacesEnabled__',
CONSUL_DATACENTER_LOCAL: '__RUNTIME_STRING_LocalDatacenter__',
}); });
break; break;
} }

View File

@ -116,6 +116,9 @@ module.exports = function(defaults) {
app.import('vendor/metrics-providers/prometheus.js', { app.import('vendor/metrics-providers/prometheus.js', {
outputFile: 'assets/metrics-providers/prometheus.js', outputFile: 'assets/metrics-providers/prometheus.js',
}); });
app.import('vendor/init.js', {
outputFile: 'assets/init.js',
});
let tree = app.toTree(); let tree = app.toTree();
return tree; return tree;
}; };

View File

@ -1,3 +1,6 @@
// rootURL in production equals `{{.ContentPath}}` and therefore is replaced
// with the value of -ui-content-path. During development rootURL uses the
// value as set in environment.js
module.exports = ({ appName, environment, rootURL, config }) => ` module.exports = ({ appName, environment, rootURL, config }) => `
<noscript> <noscript>
<div style="margin: 0 auto;"> <div style="margin: 0 auto;">
@ -5,11 +8,16 @@ module.exports = ({ appName, environment, rootURL, config }) => `
<p>Please enable JavaScript in your web browser to use Consul UI.</p> <p>Please enable JavaScript in your web browser to use Consul UI.</p>
</div> </div>
</noscript> </noscript>
<svg width="168" height="53" xmlns="http://www.w3.org/2000/svg"><g fill="#919FA8" fill-rule="evenodd"><path d="M26.078 32.12a5.586 5.586 0 1 1 5.577-5.599 5.577 5.577 0 0 1-5.577 5.6M37.009 29.328a2.56 2.56 0 1 1 2.56-2.56 2.551 2.551 0 0 1-2.56 2.56M46.916 31.669a2.56 2.56 0 1 1 .051-.21c-.028.066-.028.13-.051.21M44.588 25.068a2.565 2.565 0 0 1-2.672-.992 2.558 2.558 0 0 1-.102-2.845 2.564 2.564 0 0 1 4.676.764c.072.328.081.667.027 1a2.463 2.463 0 0 1-1.925 2.073M53.932 31.402a2.547 2.547 0 0 1-2.95 2.076 2.559 2.559 0 0 1-2.064-2.965 2.547 2.547 0 0 1 2.948-2.077 2.57 2.57 0 0 1 2.128 2.716.664.664 0 0 0-.05.228M51.857 25.103a2.56 2.56 0 1 1 2.108-2.945c.034.218.043.439.027.658a2.547 2.547 0 0 1-2.135 2.287M49.954 40.113a2.56 2.56 0 1 1 .314-1.037c-.02.366-.128.721-.314 1.037M48.974 16.893a2.56 2.56 0 1 1 .97-3.487c.264.446.375.965.317 1.479a2.56 2.56 0 0 1-1.287 2.008"/><path d="M26.526 52.603c-14.393 0-26.06-11.567-26.06-25.836C.466 12.498 12.133.931 26.526.931a25.936 25.936 0 0 1 15.836 5.307l-3.167 4.117A20.962 20.962 0 0 0 17.304 8.23C10.194 11.713 5.7 18.9 5.714 26.763c-.014 7.862 4.48 15.05 11.59 18.534a20.962 20.962 0 0 0 21.89-2.127l3.168 4.123a25.981 25.981 0 0 1-15.836 5.31z"/>${ <svg width="168" height="53" xmlns="http://www.w3.org/2000/svg"><g fill="#919FA8" fill-rule="evenodd"><path d="M26.078 32.12a5.586 5.586 0 1 1 5.577-5.599 5.577 5.577 0 0 1-5.577 5.6M37.009 29.328a2.56 2.56 0 1 1 2.56-2.56 2.551 2.551 0 0 1-2.56 2.56M46.916 31.669a2.56 2.56 0 1 1 .051-.21c-.028.066-.028.13-.051.21M44.588 25.068a2.565 2.565 0 0 1-2.672-.992 2.558 2.558 0 0 1-.102-2.845 2.564 2.564 0 0 1 4.676.764c.072.328.081.667.027 1a2.463 2.463 0 0 1-1.925 2.073M53.932 31.402a2.547 2.547 0 0 1-2.95 2.076 2.559 2.559 0 0 1-2.064-2.965 2.547 2.547 0 0 1 2.948-2.077 2.57 2.57 0 0 1 2.128 2.716.664.664 0 0 0-.05.228M51.857 25.103a2.56 2.56 0 1 1 2.108-2.945c.034.218.043.439.027.658a2.547 2.547 0 0 1-2.135 2.287M49.954 40.113a2.56 2.56 0 1 1 .314-1.037c-.02.366-.128.721-.314 1.037M48.974 16.893a2.56 2.56 0 1 1 .97-3.487c.264.446.375.965.317 1.479a2.56 2.56 0 0 1-1.287 2.008"/><path d="M26.526 52.603c-14.393 0-26.06-11.567-26.06-25.836C.466 12.498 12.133.931 26.526.931a25.936 25.936 0 0 1 15.836 5.307l-3.167 4.117A20.962 20.962 0 0 0 17.304 8.23C10.194 11.713 5.7 18.9 5.714 26.763c-.014 7.862 4.48 15.05 11.59 18.534a20.962 20.962 0 0 0 21.89-2.127l3.168 4.123a25.981 25.981 0 0 1-15.836 5.31z"/>${
config.CONSUL_BINARY_TYPE !== 'oss' && config.CONSUL_BINARY_TYPE !== '' config.CONSUL_BINARY_TYPE !== 'oss' && config.CONSUL_BINARY_TYPE !== ''
? `<path data-enterprise-logo d="M61 42.083h3.975v.785H61.87v2.136h2.882v.784H61.87v2.31h3.114v.785H61v-6.8zm6.907 1.018V48.9h-.828v-6.817h1.2l2.94 5.84v-5.84h.829V48.9h-1.193L67.907 43.1zm7.826-.225h-2.012v-.784h4.911v.784h-2.02V48.9h-.879v-6.024zm4.564-.793h3.975v.785h-3.106v2.136h2.882v.784h-2.882v2.31h3.114v.785h-3.992l.009-6.8zm8.605 4.347h-1.657v2.503h-.87v-6.85h2.576c1.45 0 1.963.635 1.963 1.67v.984a1.435 1.435 0 0 1-1.077 1.585l1.756 2.57h-1.002l-1.69-2.462zm0-3.562h-1.657v2.778h1.657c.828 0 1.118-.234 1.118-.901v-.968c.024-.676-.265-.901-1.094-.901l-.024-.008zm4.488-.785h2.485c1.45 0 1.963.635 1.963 1.67v1.009c0 1.05-.505 1.668-1.963 1.668H94.3v2.47h-.87l-.04-6.817zm2.419.785h-1.54v2.803h1.54c.828 0 1.118-.234 1.118-.901v-1.001c0-.668-.282-.893-1.118-.893v-.008zm6.368 3.562h-1.656v2.503h-.87v-6.85h2.576c1.45 0 1.963.635 1.963 1.67v.984a1.435 1.435 0 0 1-1.077 1.585l1.756 2.57h-1.002l-1.69-2.462zm0-3.562h-1.656v2.778h1.656c.829 0 1.118-.234 1.118-.901v-.968c.017-.676-.265-.901-1.101-.901l-.017-.008zm5.392 6.032h-.828v-6.817h.828V48.9zm4.14.1a5.76 5.76 0 0 1-2.012-.359l.141-.717c.605.195 1.236.3 1.872.308 1.085 0 1.308-.283 1.308-1.06 0-.917 0-1-1.4-1.317-1.656-.368-1.83-.685-1.83-2.095 0-1.184.49-1.76 2.162-1.76a7.648 7.648 0 0 1 1.83.225l-.074.743a8.223 8.223 0 0 0-1.74-.192c-1.11 0-1.308.225-1.308 1.01 0 .942 0 .984 1.342 1.318 1.797.45 1.888.717 1.888 2.044.033 1.176-.315 1.852-2.178 1.852zm4.332-6.917h3.95v.785h-3.105v2.136h2.882v.784h-2.882v2.31H120v.785h-3.992l.033-6.8z" fill-rule="nonzero"/>` ? `<path data-enterprise-logo d="M61 42.083h3.975v.785H61.87v2.136h2.882v.784H61.87v2.31h3.114v.785H61v-6.8zm6.907 1.018V48.9h-.828v-6.817h1.2l2.94 5.84v-5.84h.829V48.9h-1.193L67.907 43.1zm7.826-.225h-2.012v-.784h4.911v.784h-2.02V48.9h-.879v-6.024zm4.564-.793h3.975v.785h-3.106v2.136h2.882v.784h-2.882v2.31h3.114v.785h-3.992l.009-6.8zm8.605 4.347h-1.657v2.503h-.87v-6.85h2.576c1.45 0 1.963.635 1.963 1.67v.984a1.435 1.435 0 0 1-1.077 1.585l1.756 2.57h-1.002l-1.69-2.462zm0-3.562h-1.657v2.778h1.657c.828 0 1.118-.234 1.118-.901v-.968c.024-.676-.265-.901-1.094-.901l-.024-.008zm4.488-.785h2.485c1.45 0 1.963.635 1.963 1.67v1.009c0 1.05-.505 1.668-1.963 1.668H94.3v2.47h-.87l-.04-6.817zm2.419.785h-1.54v2.803h1.54c.828 0 1.118-.234 1.118-.901v-1.001c0-.668-.282-.893-1.118-.893v-.008zm6.368 3.562h-1.656v2.503h-.87v-6.85h2.576c1.45 0 1.963.635 1.963 1.67v.984a1.435 1.435 0 0 1-1.077 1.585l1.756 2.57h-1.002l-1.69-2.462zm0-3.562h-1.656v2.778h1.656c.829 0 1.118-.234 1.118-.901v-.968c.017-.676-.265-.901-1.101-.901l-.017-.008zm5.392 6.032h-.828v-6.817h.828V48.9zm4.14.1a5.76 5.76 0 0 1-2.012-.359l.141-.717c.605.195 1.236.3 1.872.308 1.085 0 1.308-.283 1.308-1.06 0-.917 0-1-1.4-1.317-1.656-.368-1.83-.685-1.83-2.095 0-1.184.49-1.76 2.162-1.76a7.648 7.648 0 0 1 1.83.225l-.074.743a8.223 8.223 0 0 0-1.74-.192c-1.11 0-1.308.225-1.308 1.01 0 .942 0 .984 1.342 1.318 1.797.45 1.888.717 1.888 2.044.033 1.176-.315 1.852-2.178 1.852zm4.332-6.917h3.95v.785h-3.105v2.136h2.882v.784h-2.882v2.31H120v.785h-3.992l.033-6.8z" fill-rule="nonzero"/>`
: `` : ``
}<path d="M61 30.15V17.948c0-4.962 2.845-7.85 9.495-7.85 2.484 0 5.048.326 7.252.895l-.561 4.433c-2.164-.406-4.688-.691-6.53-.691-3.486 0-4.608 1.22-4.608 4.108v10.412c0 2.888 1.122 4.108 4.607 4.108 1.843 0 4.367-.284 6.53-.691l.562 4.433c-2.204.57-4.768.895-7.252.895C63.845 38 61 35.112 61 30.15zm36.808.04c0 4.068-1.802 7.81-8.493 7.81-6.69 0-8.494-3.742-8.494-7.81v-5.002c0-4.067 1.803-7.81 8.494-7.81 6.69 0 8.493 3.743 8.493 7.81v5.003zm-4.887-5.165c0-2.237-1.002-3.416-3.606-3.416s-3.606 1.18-3.606 3.416v5.328c0 2.237 1.002 3.417 3.606 3.417s3.606-1.18 3.606-3.417v-5.328zm25.79 12.568h-4.887V23.764c0-1.057-.44-1.586-1.563-1.586-1.201 0-3.325.732-5.088 1.668v13.747h-4.887V17.785h3.726l.48 1.668c2.444-1.22 5.53-2.074 7.813-2.074 3.245 0 4.407 2.318 4.407 5.857v14.357zm18.26-5.775c0 3.823-1.162 6.182-7.052 6.182-2.083 0-4.927-.488-6.73-1.139l.68-3.782c1.643.488 3.807.854 5.81.854 2.164 0 2.484-.488 2.484-1.993 0-1.22-.24-1.83-3.405-2.603-4.768-1.18-5.329-2.4-5.329-6.223 0-3.986 1.723-5.735 7.292-5.735 1.803 0 4.166.244 5.85.691l-.482 3.945c-1.482-.284-3.846-.569-5.368-.569-2.124 0-2.484.488-2.484 1.708 0 1.587.12 1.709 2.764 2.4 5.449 1.464 5.97 2.196 5.97 6.264zm4.357-14.033h4.887v13.83c0 1.057.441 1.586 1.563 1.586 1.202 0 3.325-.733 5.088-1.668V17.785h4.888v19.808h-3.726l-.481-1.667c-2.444 1.22-5.529 2.074-7.812 2.074-3.246 0-4.407-2.318-4.407-5.857V17.785zM168 37.593h-4.888V9.691L168 9v28.593z"/></g></svg> }<path d="M61 30.15V17.948c0-4.962 2.845-7.85 9.495-7.85 2.484 0 5.048.326 7.252.895l-.561 4.433c-2.164-.406-4.688-.691-6.53-.691-3.486 0-4.608 1.22-4.608 4.108v10.412c0 2.888 1.122 4.108 4.607 4.108 1.843 0 4.367-.284 6.53-.691l.562 4.433c-2.204.57-4.768.895-7.252.895C63.845 38 61 35.112 61 30.15zm36.808.04c0 4.068-1.802 7.81-8.493 7.81-6.69 0-8.494-3.742-8.494-7.81v-5.002c0-4.067 1.803-7.81 8.494-7.81 6.69 0 8.493 3.743 8.493 7.81v5.003zm-4.887-5.165c0-2.237-1.002-3.416-3.606-3.416s-3.606 1.18-3.606 3.416v5.328c0 2.237 1.002 3.417 3.606 3.417s3.606-1.18 3.606-3.417v-5.328zm25.79 12.568h-4.887V23.764c0-1.057-.44-1.586-1.563-1.586-1.201 0-3.325.732-5.088 1.668v13.747h-4.887V17.785h3.726l.48 1.668c2.444-1.22 5.53-2.074 7.813-2.074 3.245 0 4.407 2.318 4.407 5.857v14.357zm18.26-5.775c0 3.823-1.162 6.182-7.052 6.182-2.083 0-4.927-.488-6.73-1.139l.68-3.782c1.643.488 3.807.854 5.81.854 2.164 0 2.484-.488 2.484-1.993 0-1.22-.24-1.83-3.405-2.603-4.768-1.18-5.329-2.4-5.329-6.223 0-3.986 1.723-5.735 7.292-5.735 1.803 0 4.166.244 5.85.691l-.482 3.945c-1.482-.284-3.846-.569-5.368-.569-2.124 0-2.484.488-2.484 1.708 0 1.587.12 1.709 2.764 2.4 5.449 1.464 5.97 2.196 5.97 6.264zm4.357-14.033h4.887v13.83c0 1.057.441 1.586 1.563 1.586 1.202 0 3.325-.733 5.088-1.668V17.785h4.888v19.808h-3.726l-.481-1.667c-2.444 1.22-5.529 2.074-7.812 2.074-3.246 0-4.407-2.318-4.407-5.857V17.785zM168 37.593h-4.888V9.691L168 9v28.593z"/></g></svg>
<script type="application/json" data-consul-ui-config>
${environment === 'production' ? `{{jsonEncode .}}` : JSON.stringify(config.operatorConfig)}
</script>
<script src="${rootURL}assets/init.js"></script>
<script src="${rootURL}assets/vendor.js"></script> <script src="${rootURL}assets/vendor.js"></script>
${environment === 'test' ? `<script src="${rootURL}assets/test-support.js"></script>` : ``} ${environment === 'test' ? `<script src="${rootURL}assets/test-support.js"></script>` : ``}
<script> <script>

View File

@ -1,11 +1,9 @@
// rootURL in production equals `{{.ContentPath}}` and therefore is replaced
// with the value of -ui-content-path. During development rootURL uses the
// value as set in environment.js
module.exports = ({ appName, environment, rootURL, config }) => ` module.exports = ({ appName, environment, rootURL, config }) => `
<!-- CONSUL_VERSION: ${config.CONSUL_VERSION} --> <!-- CONSUL_VERSION: ${config.CONSUL_VERSION} -->
<meta name="consul-ui/ui_config" content="${
environment === 'production'
? `{{ jsonEncodeAndEscape .UIConfig }}`
: escape(JSON.stringify({}))
}" />
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32"> <link rel="icon" type="image/png" href="${rootURL}assets/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16"> <link rel="icon" type="image/png" href="${rootURL}assets/favicon-16x16.png" sizes="16x16">
<link integrity="" rel="stylesheet" href="${rootURL}assets/vendor.css"> <link integrity="" rel="stylesheet" href="${rootURL}assets/vendor.css">

View File

@ -11,16 +11,17 @@ test(
{ {
environment: 'production', environment: 'production',
CONSUL_BINARY_TYPE: 'oss', CONSUL_BINARY_TYPE: 'oss',
CONSUL_ACLS_ENABLED: '__RUNTIME_BOOL_ACLsEnabled__', operatorConfig: {}
CONSUL_SSO_ENABLED: '__RUNTIME_BOOL_SSOEnabled__',
CONSUL_NSPACES_ENABLED: '__RUNTIME_BOOL_NamespacesEnabled__',
}, },
{ {
environment: 'test', environment: 'test',
CONSUL_BINARY_TYPE: 'oss', CONSUL_BINARY_TYPE: 'oss',
CONSUL_ACLS_ENABLED: true, operatorConfig: {
CONSUL_NSPACES_ENABLED: false, ACLsEnabled: true,
CONSUL_SSO_ENABLED: false, NamespacesEnabled: false,
SSOEnabled: false,
LocalDatacenter: 'dc1',
}
}, },
{ {
$: { $: {
@ -28,9 +29,12 @@ test(
}, },
environment: 'test', environment: 'test',
CONSUL_BINARY_TYPE: 'oss', CONSUL_BINARY_TYPE: 'oss',
CONSUL_ACLS_ENABLED: true, operatorConfig: {
CONSUL_NSPACES_ENABLED: true, ACLsEnabled: true,
CONSUL_SSO_ENABLED: false, NamespacesEnabled: true,
SSOEnabled: false,
LocalDatacenter: 'dc1',
}
}, },
{ {
$: { $: {
@ -38,16 +42,22 @@ test(
}, },
environment: 'test', environment: 'test',
CONSUL_BINARY_TYPE: 'oss', CONSUL_BINARY_TYPE: 'oss',
CONSUL_ACLS_ENABLED: true, operatorConfig: {
CONSUL_NSPACES_ENABLED: false, ACLsEnabled: true,
CONSUL_SSO_ENABLED: true, NamespacesEnabled: false,
SSOEnabled: true,
LocalDatacenter: 'dc1',
}
}, },
{ {
environment: 'staging', environment: 'staging',
CONSUL_BINARY_TYPE: 'oss', CONSUL_BINARY_TYPE: 'oss',
CONSUL_ACLS_ENABLED: true, operatorConfig: {
CONSUL_NSPACES_ENABLED: true, ACLsEnabled: true,
CONSUL_SSO_ENABLED: true, NamespacesEnabled: true,
SSOEnabled: true,
LocalDatacenter: 'dc1',
}
} }
].forEach( ].forEach(
function(item) { function(item) {
@ -57,7 +67,7 @@ test(
if(key === '$') { if(key === '$') {
return; return;
} }
t.equal( t.deepEqual(
env[key], env[key],
item[key], item[key],
`Expect ${key} to equal ${item[key]} in the ${item.environment} environment ${typeof item.$ !== 'undefined' ? `(with ${JSON.stringify(item.$)})` : ''}` `Expect ${key} to equal ${item[key]} in the ${item.environment} environment ${typeof item.$ !== 'undefined' ? `(with ${JSON.stringify(item.$)})` : ''}`

View File

@ -19,6 +19,11 @@ const makeGetElementsBy = function(str) {
]; ];
}; };
}; };
const makeOperatorConfig = function(json) {
return {
textContent: JSON.stringify(json),
};
};
const win = { const win = {
performance: { performance: {
getEntriesByType: getEntriesByType, getEntriesByType: getEntriesByType,
@ -34,6 +39,7 @@ const doc = {
cookie: '', cookie: '',
getElementsByTagName: makeGetElementsBy(''), getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({}),
}; };
module('Unit | Utility | getEnvironment', function() { module('Unit | Utility | getEnvironment', function() {
test('it returns a function', function(assert) { test('it returns a function', function(assert) {
@ -62,6 +68,7 @@ module('Unit | Utility | getEnvironment', function() {
cookie: '', cookie: '',
getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`), getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({}),
}; };
let env = getEnvironment(config, win, doc); let env = getEnvironment(config, win, doc);
assert.equal(env('CONSUL_BASE_UI_URL'), expected); assert.equal(env('CONSUL_BASE_UI_URL'), expected);
@ -70,6 +77,7 @@ module('Unit | Utility | getEnvironment', function() {
cookie: '', cookie: '',
getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`), getElementsByTagName: makeGetElementsBy(`${expected}/assets/consul-ui.js`),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({}),
}; };
env = getEnvironment(config, win, doc); env = getEnvironment(config, win, doc);
assert.equal(env('CONSUL_BASE_UI_URL'), expected); assert.equal(env('CONSUL_BASE_UI_URL'), expected);
@ -144,6 +152,7 @@ module('Unit | Utility | getEnvironment', function() {
cookie: 'CONSUL_NSPACES_ENABLE=1', cookie: 'CONSUL_NSPACES_ENABLE=1',
getElementsByTagName: makeGetElementsBy(''), getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({ NamespacesEnabled: true }),
}; };
let env = getEnvironment(config, win, doc); let env = getEnvironment(config, win, doc);
assert.ok(env('CONSUL_NSPACES_ENABLED')); assert.ok(env('CONSUL_NSPACES_ENABLED'));
@ -155,6 +164,7 @@ module('Unit | Utility | getEnvironment', function() {
cookie: 'CONSUL_NSPACES_ENABLE=0', cookie: 'CONSUL_NSPACES_ENABLE=0',
getElementsByTagName: makeGetElementsBy(''), getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({ NamespacesEnabled: false }),
}; };
env = getEnvironment(config, win, doc); env = getEnvironment(config, win, doc);
assert.notOk(env('CONSUL_NSPACES_ENABLED')); assert.notOk(env('CONSUL_NSPACES_ENABLED'));
@ -180,6 +190,7 @@ module('Unit | Utility | getEnvironment', function() {
cookie: 'CONSUL_NSPACES_ENABLE=1', cookie: 'CONSUL_NSPACES_ENABLE=1',
getElementsByTagName: makeGetElementsBy(''), getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({ NamespacesEnabled: false }),
}; };
let env = getEnvironment(config, win, doc); let env = getEnvironment(config, win, doc);
assert.notOk(env('CONSUL_NSPACES_ENABLED')); assert.notOk(env('CONSUL_NSPACES_ENABLED'));
@ -191,6 +202,7 @@ module('Unit | Utility | getEnvironment', function() {
cookie: 'CONSUL_NSPACES_ENABLE=0', cookie: 'CONSUL_NSPACES_ENABLE=0',
getElementsByTagName: makeGetElementsBy(''), getElementsByTagName: makeGetElementsBy(''),
getElementsByName: makeGetElementsBy('{}'), getElementsByName: makeGetElementsBy('{}'),
querySelector: () => makeOperatorConfig({ NamespacesEnabled: true }),
}; };
env = getEnvironment(config, win, doc); env = getEnvironment(config, win, doc);
assert.ok(env('CONSUL_NSPACES_ENABLED')); assert.ok(env('CONSUL_NSPACES_ENABLED'));

24
ui/packages/consul-ui/vendor/init.js vendored Normal file
View File

@ -0,0 +1,24 @@
(function(doc, appName) {
try {
const $appMeta = doc.querySelector(`[name="${appName}/config/environment"]`);
// pick out the operatorConfig from our application/json script tag
const operatorConfig = JSON.parse(doc.querySelector(`[data-${appName}-config]`).textContent);
// pick out the ember config from its meta tag
const emberConfig = JSON.parse(decodeURIComponent($appMeta.getAttribute('content')));
// rootURL is a special variable that requires settings before ember
// boots via ember's HTML metadata tag, the variable is equivalent to
// the -ui-content-path Consul flag (or `ui_config { content_path = ""}`)
// There will potentially be one or two more 'pre-init' variables that we need.
// Anything not 'pre-init' should use ui_config.
// Check the value to make sure its there and a string
const rootURL =
typeof operatorConfig.ContentPath !== 'string' ? '' : operatorConfig.ContentPath;
if (rootURL.length > 0) {
emberConfig.rootURL = rootURL;
}
$appMeta.setAttribute('content', encodeURIComponent(JSON.stringify(emberConfig)));
} catch (e) {
throw new Error(`Unable to parse ${appName} settings: ${e.message}`);
}
})(document, 'consul-ui');

2
vendor/modules.txt vendored
View File

@ -484,6 +484,8 @@ golang.org/x/lint/golint
golang.org/x/net/bpf golang.org/x/net/bpf
golang.org/x/net/context golang.org/x/net/context
golang.org/x/net/context/ctxhttp golang.org/x/net/context/ctxhttp
golang.org/x/net/html
golang.org/x/net/html/atom
golang.org/x/net/http/httpguts golang.org/x/net/http/httpguts
golang.org/x/net/http2 golang.org/x/net/http2
golang.org/x/net/http2/hpack golang.org/x/net/http2/hpack