diff --git a/.circleci/config.yml b/.circleci/config.yml
index f0778697c2..768eb72688 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -626,7 +626,7 @@ jobs:
- persist_to_workspace:
root: .
paths:
- - ./agent/bindata_assetfs.go
+ - ./agent/uiserver/bindata_assetfs.go
- run: *notify-slack-failure
# commits static assets to git
@@ -641,16 +641,16 @@ jobs:
- attach_workspace:
at: .
- run:
- name: commit agent/bindata_assetfs.go if there are changes
+ name: commit agent/uiserver/bindata_assetfs.go if there are changes
command: |
exit 0
- if ! git diff --exit-code agent/bindata_assetfs.go; then
+ if ! git diff --exit-code agent/uiserver/bindata_assetfs.go; then
git config --local user.email "hashicorp-ci@users.noreply.github.com"
git config --local user.name "hashicorp-ci"
short_sha=$(git rev-parse --short HEAD)
- git add agent/bindata_assetfs.go
- git commit -m "auto-updated agent/bindata_assetfs.go from commit ${short_sha}"
+ git add agent/uiserver/bindata_assetfs.go
+ git commit -m "auto-updated agent/uiserver/bindata_assetfs.go from commit ${short_sha}"
git push origin master
else
echo "no new static assets to publish"
diff --git a/GNUmakefile b/GNUmakefile
index b9540879ee..9fdfab8ec8 100644
--- a/GNUmakefile
+++ b/GNUmakefile
@@ -16,7 +16,7 @@ GOARCH?=$(shell go env GOARCH)
GOPATH=$(shell go env GOPATH)
MAIN_GOPATH=$(shell go env GOPATH | cut -d: -f1)
-ASSETFS_PATH?=agent/bindata_assetfs.go
+ASSETFS_PATH?=agent/uiserver/bindata_assetfs.go
# Get the git commit
GIT_COMMIT?=$(shell git rev-parse --short HEAD)
GIT_COMMIT_YEAR?=$(shell git show -s --format=%cd --date=format:%Y HEAD)
@@ -306,7 +306,7 @@ lint:
# also run as part of the release build script when it verifies that there are no
# changes to the UI assets that aren't checked in.
static-assets:
- @go-bindata-assetfs -modtime 1 -pkg agent -prefix pkg -o $(ASSETFS_PATH) ./pkg/web_ui/...
+ @go-bindata-assetfs -modtime 1 -pkg uiserver -prefix pkg -o $(ASSETFS_PATH) ./pkg/web_ui/...
@go fmt $(ASSETFS_PATH)
diff --git a/agent/agent.go b/agent/agent.go
index 66827a8821..a74eae9ce4 100644
--- a/agent/agent.go
+++ b/agent/agent.go
@@ -255,6 +255,23 @@ type Agent struct {
// fail, the agent will be shutdown.
apiServers *apiServers
+ // httpHandlers provides direct access to (one of) the HTTPHandlers started by
+ // this agent. This is used in tests to test HTTP endpoints without overhead
+ // of TCP connections etc.
+ //
+ // TODO: this is a temporary re-introduction after we removed a list of
+ // HTTPServers in favour of apiServers abstraction. Now that HTTPHandlers is
+ // stateful and has config reloading though it's not OK to just use a
+ // different instance of handlers in tests to the ones that the agent is wired
+ // up to since then config reloads won't actually affect the handlers under
+ // test while plumbing the external handlers in the TestAgent through bypasses
+ // testing that the agent itself is actually reloading the state correctly.
+ // Once we move `apiServers` to be a passed-in dependency for NewAgent, we
+ // should be able to remove this and have the Test Agent create the
+ // HTTPHandlers and pass them in removing the need to pull them back out
+ // again.
+ httpHandlers *HTTPHandlers
+
// wgServers is the wait group for all HTTP and DNS servers
// TODO: remove once dnsServers are handled by apiServers
wgServers sync.WaitGroup
@@ -290,6 +307,10 @@ type Agent struct {
// IP.
httpConnLimiter connlimit.Limiter
+ // configReloaders are subcomponents that need to be notified on a reload so
+ // they can update their internal state.
+ configReloaders []ConfigReloader
+
// enterpriseAgent embeds fields that we only access in consul-enterprise builds
enterpriseAgent
}
@@ -735,6 +756,8 @@ func (a *Agent) listenHTTP() ([]apiServer, error) {
agent: a,
denylist: NewDenylist(a.config.HTTPBlockEndpoints),
}
+ a.configReloaders = append(a.configReloaders, srv.ReloadConfig)
+ a.httpHandlers = srv
httpServer := &http.Server{
Addr: l.Addr().String(),
TLSConfig: tlscfg,
@@ -3571,6 +3594,12 @@ func (a *Agent) reloadConfigInternal(newCfg *config.RuntimeConfig) error {
a.State.SetDiscardCheckOutput(newCfg.DiscardCheckOutput)
+ for _, r := range a.configReloaders {
+ if err := r(newCfg); err != nil {
+ return err
+ }
+ }
+
return nil
}
diff --git a/agent/config/builder.go b/agent/config/builder.go
index 4b9aab1b7d..666ff0b655 100644
--- a/agent/config/builder.go
+++ b/agent/config/builder.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io/ioutil"
"net"
+ "net/url"
"os"
"path/filepath"
"reflect"
@@ -797,6 +798,26 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
return RuntimeConfig{}, fmt.Errorf("serf_wan_allowed_cidrs: %s", err)
}
+ // Handle Deprecated UI config fields
+ if c.UI != nil {
+ b.warn("The 'ui' field is deprecated. Use the 'ui_config.enabled' field instead.")
+ if c.UIConfig.Enabled == nil {
+ c.UIConfig.Enabled = c.UI
+ }
+ }
+ if c.UIDir != nil {
+ b.warn("The 'ui_dir' field is deprecated. Use the 'ui_config.dir' field instead.")
+ if c.UIConfig.Dir == nil {
+ c.UIConfig.Dir = c.UIDir
+ }
+ }
+ if c.UIContentPath != nil {
+ b.warn("The 'ui_content_path' field is deprecated. Use the 'ui_config.content_path' field instead.")
+ if c.UIConfig.ContentPath == nil {
+ c.UIConfig.ContentPath = c.UIContentPath
+ }
+ }
+
// ----------------------------------------------------------------
// build runtime config
//
@@ -981,19 +1002,17 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
EnableDebug: b.boolVal(c.EnableDebug),
EnableRemoteScriptChecks: enableRemoteScriptChecks,
EnableLocalScriptChecks: enableLocalScriptChecks,
-
- EnableUI: b.boolVal(c.UI),
- EncryptKey: b.stringVal(c.EncryptKey),
- EncryptVerifyIncoming: b.boolVal(c.EncryptVerifyIncoming),
- EncryptVerifyOutgoing: b.boolVal(c.EncryptVerifyOutgoing),
- GRPCPort: grpcPort,
- GRPCAddrs: grpcAddrs,
- HTTPMaxConnsPerClient: b.intVal(c.Limits.HTTPMaxConnsPerClient),
- HTTPSHandshakeTimeout: b.durationVal("limits.https_handshake_timeout", c.Limits.HTTPSHandshakeTimeout),
- KeyFile: b.stringVal(c.KeyFile),
- KVMaxValueSize: b.uint64Val(c.Limits.KVMaxValueSize),
- LeaveDrainTime: b.durationVal("performance.leave_drain_time", c.Performance.LeaveDrainTime),
- LeaveOnTerm: leaveOnTerm,
+ EncryptKey: b.stringVal(c.EncryptKey),
+ EncryptVerifyIncoming: b.boolVal(c.EncryptVerifyIncoming),
+ EncryptVerifyOutgoing: b.boolVal(c.EncryptVerifyOutgoing),
+ GRPCPort: grpcPort,
+ GRPCAddrs: grpcAddrs,
+ HTTPMaxConnsPerClient: b.intVal(c.Limits.HTTPMaxConnsPerClient),
+ HTTPSHandshakeTimeout: b.durationVal("limits.https_handshake_timeout", c.Limits.HTTPSHandshakeTimeout),
+ KeyFile: b.stringVal(c.KeyFile),
+ KVMaxValueSize: b.uint64Val(c.Limits.KVMaxValueSize),
+ LeaveDrainTime: b.durationVal("performance.leave_drain_time", c.Performance.LeaveDrainTime),
+ LeaveOnTerm: leaveOnTerm,
Logging: logging.Config{
LogLevel: b.stringVal(c.LogLevel),
LogJSON: b.boolVal(c.LogJSON),
@@ -1058,8 +1077,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
TaggedAddresses: c.TaggedAddresses,
TranslateWANAddrs: b.boolVal(c.TranslateWANAddrs),
TxnMaxReqLen: b.uint64Val(c.Limits.TxnMaxReqLen),
- UIDir: b.stringVal(c.UIDir),
- UIContentPath: UIPathBuilder(b.stringVal(c.UIContentPath)),
+ UIConfig: b.uiConfigVal(c.UIConfig),
UnixSocketGroup: b.stringVal(c.UnixSocket.Group),
UnixSocketMode: b.stringVal(c.UnixSocket.Mode),
UnixSocketUser: b.stringVal(c.UnixSocket.User),
@@ -1091,10 +1109,26 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
return rt, nil
}
+// reBasicName validates that a field contains only lower case alphanumerics,
+// underscore and dash and is non-empty.
+var reBasicName = regexp.MustCompile("^[a-z0-9_-]+$")
+
+func validateBasicName(field, value string, allowEmpty bool) error {
+ if value == "" {
+ if allowEmpty {
+ return nil
+ }
+ return fmt.Errorf("%s cannot be empty", field)
+ }
+ if !reBasicName.MatchString(value) {
+ return fmt.Errorf("%s can only contain lowercase alphanumeric, - or _ characters."+
+ " received: %q", field, value)
+ }
+ return nil
+}
+
// Validate performs semantic validation of the runtime configuration.
func (b *Builder) Validate(rt RuntimeConfig) error {
- // reDatacenter defines a regexp for a valid datacenter name
- var reDatacenter = regexp.MustCompile("^[a-z0-9_-]+$")
// validContentPath defines a regexp for a valid content path name.
var validContentPath = regexp.MustCompile(`^[A-Za-z0-9/_-]+$`)
@@ -1103,22 +1137,53 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
// check required params we cannot recover from first
//
- if rt.Datacenter == "" {
- return fmt.Errorf("datacenter cannot be empty")
- }
- if !reDatacenter.MatchString(rt.Datacenter) {
- return fmt.Errorf("datacenter cannot be %q. Please use only [a-z0-9-_]", rt.Datacenter)
+ if err := validateBasicName("datacenter", rt.Datacenter, false); err != nil {
+ return err
}
if rt.DataDir == "" && !rt.DevMode {
return fmt.Errorf("data_dir cannot be empty")
}
- if !validContentPath.MatchString(rt.UIContentPath) {
- return fmt.Errorf("ui-content-path can only contain alphanumeric, -, _, or /. received: %s", rt.UIContentPath)
+ if !validContentPath.MatchString(rt.UIConfig.ContentPath) {
+ return fmt.Errorf("ui-content-path can only contain alphanumeric, -, _, or /. received: %q", rt.UIConfig.ContentPath)
}
- if hasVersion.MatchString(rt.UIContentPath) {
- return fmt.Errorf("ui-content-path cannot have 'v[0-9]'. received: %s", rt.UIContentPath)
+ if hasVersion.MatchString(rt.UIConfig.ContentPath) {
+ return fmt.Errorf("ui-content-path cannot have 'v[0-9]'. received: %q", rt.UIConfig.ContentPath)
+ }
+
+ if err := validateBasicName("ui_config.metrics_provider", rt.UIConfig.MetricsProvider, true); err != nil {
+ return err
+ }
+ if rt.UIConfig.MetricsProviderOptionsJSON != "" {
+ // Attempt to parse the JSON to ensure it's valid, parsing into a map
+ // ensures we get an object.
+ var dummyMap map[string]interface{}
+ err := json.Unmarshal([]byte(rt.UIConfig.MetricsProviderOptionsJSON), &dummyMap)
+ if err != nil {
+ return fmt.Errorf("ui_config.metrics_provider_options_json must be empty "+
+ "or a string containing a valid JSON object. received: %q",
+ rt.UIConfig.MetricsProviderOptionsJSON)
+ }
+ }
+ if rt.UIConfig.MetricsProxy.BaseURL != "" {
+ u, err := url.Parse(rt.UIConfig.MetricsProxy.BaseURL)
+ if err != nil || !(u.Scheme == "http" || u.Scheme == "https") {
+ return fmt.Errorf("ui_config.metrics_proxy.base_url must be a valid http"+
+ " or https URL. received: %q",
+ rt.UIConfig.MetricsProxy.BaseURL)
+ }
+ }
+ for k, v := range rt.UIConfig.DashboardURLTemplates {
+ if err := validateBasicName("ui_config.dashboard_url_templates key names", k, false); err != nil {
+ return err
+ }
+ u, err := url.Parse(v)
+ if err != nil || !(u.Scheme == "http" || u.Scheme == "https") {
+ return fmt.Errorf("ui_config.dashboard_url_templates values must be a"+
+ " valid http or https URL. received: %q",
+ rt.UIConfig.MetricsProxy.BaseURL)
+ }
}
if !rt.DevMode {
@@ -1190,15 +1255,15 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
if rt.AutopilotMaxTrailingLogs < 0 {
return fmt.Errorf("autopilot.max_trailing_logs cannot be %d. Must be greater than or equal to zero", rt.AutopilotMaxTrailingLogs)
}
- if rt.ACLDatacenter != "" && !reDatacenter.MatchString(rt.ACLDatacenter) {
- return fmt.Errorf("acl_datacenter cannot be %q. Please use only [a-z0-9-_]", rt.ACLDatacenter)
+ if err := validateBasicName("acl_datacenter", rt.ACLDatacenter, true); err != nil {
+ return err
}
// In DevMode, UI is enabled by default, so to enable rt.UIDir, don't perform this check
- if !rt.DevMode && rt.EnableUI && rt.UIDir != "" {
+ if !rt.DevMode && rt.UIConfig.Enabled && rt.UIConfig.Dir != "" {
return fmt.Errorf(
- "Both the ui and ui-dir flags were specified, please provide only one.\n" +
- "If trying to use your own web UI resources, use the ui-dir flag.\n" +
- "The web UI is included in the binary so use ui to enable it")
+ "Both the ui_config.enabled and ui_config.dir (or -ui and -ui-dir) were specified, please provide only one.\n" +
+ "If trying to use your own web UI resources, use ui_config.dir or the -ui-dir flag.\n" +
+ "The web UI is included in the binary so use ui_config.enabled or the -ui flag to enable it")
}
if rt.DNSUDPAnswerLimit < 0 {
return fmt.Errorf("dns_config.udp_answer_limit cannot be %d. Must be greater than or equal to zero", rt.DNSUDPAnswerLimit)
@@ -1647,6 +1712,35 @@ func (b *Builder) serviceConnectVal(v *ServiceConnect) *structs.ServiceConnect {
}
}
+func (b *Builder) uiConfigVal(v RawUIConfig) UIConfig {
+ return UIConfig{
+ Enabled: b.boolVal(v.Enabled),
+ Dir: b.stringVal(v.Dir),
+ ContentPath: UIPathBuilder(b.stringVal(v.ContentPath)),
+ MetricsProvider: b.stringVal(v.MetricsProvider),
+ MetricsProviderFiles: v.MetricsProviderFiles,
+ MetricsProviderOptionsJSON: b.stringVal(v.MetricsProviderOptionsJSON),
+ MetricsProxy: b.uiMetricsProxyVal(v.MetricsProxy),
+ DashboardURLTemplates: v.DashboardURLTemplates,
+ }
+}
+
+func (b *Builder) uiMetricsProxyVal(v RawUIMetricsProxy) UIMetricsProxy {
+ var hdrs []UIMetricsProxyAddHeader
+
+ for _, hdr := range v.AddHeaders {
+ hdrs = append(hdrs, UIMetricsProxyAddHeader{
+ Name: b.stringVal(hdr.Name),
+ Value: b.stringVal(hdr.Value),
+ })
+ }
+
+ return UIMetricsProxy{
+ BaseURL: b.stringVal(v.BaseURL),
+ AddHeaders: hdrs,
+ }
+}
+
func (b *Builder) boolValWithDefault(v *bool, defaultVal bool) bool {
if v == nil {
return defaultVal
diff --git a/agent/config/config.go b/agent/config/config.go
index 3e0710380b..b66bcd0d81 100644
--- a/agent/config/config.go
+++ b/agent/config/config.go
@@ -133,123 +133,129 @@ type Config struct {
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl.tokens" stanza
ACLTTL *string `json:"acl_ttl,omitempty" hcl:"acl_ttl" mapstructure:"acl_ttl"`
// DEPRECATED (ACL-Legacy-Compat) - moved into the "acl.tokens" stanza
- ACLToken *string `json:"acl_token,omitempty" hcl:"acl_token" mapstructure:"acl_token"`
- ACL ACL `json:"acl,omitempty" hcl:"acl" mapstructure:"acl"`
- Addresses Addresses `json:"addresses,omitempty" hcl:"addresses" mapstructure:"addresses"`
- AdvertiseAddrLAN *string `json:"advertise_addr,omitempty" hcl:"advertise_addr" mapstructure:"advertise_addr"`
- AdvertiseAddrLANIPv4 *string `json:"advertise_addr_ipv4,omitempty" hcl:"advertise_addr_ipv4" mapstructure:"advertise_addr_ipv4"`
- AdvertiseAddrLANIPv6 *string `json:"advertise_addr_ipv6,omitempty" hcl:"advertise_addr_ipv6" mapstructure:"advertise_addr_ipv6"`
- AdvertiseAddrWAN *string `json:"advertise_addr_wan,omitempty" hcl:"advertise_addr_wan" mapstructure:"advertise_addr_wan"`
- AdvertiseAddrWANIPv4 *string `json:"advertise_addr_wan_ipv4,omitempty" hcl:"advertise_addr_wan_ipv4" mapstructure:"advertise_addr_wan_ipv4"`
- AdvertiseAddrWANIPv6 *string `json:"advertise_addr_wan_ipv6,omitempty" hcl:"advertise_addr_wan_ipv6" mapstructure:"advertise_addr_ipv6"`
- AutoConfig AutoConfigRaw `json:"auto_config,omitempty" hcl:"auto_config" mapstructure:"auto_config"`
- Autopilot Autopilot `json:"autopilot,omitempty" hcl:"autopilot" mapstructure:"autopilot"`
- BindAddr *string `json:"bind_addr,omitempty" hcl:"bind_addr" mapstructure:"bind_addr"`
- Bootstrap *bool `json:"bootstrap,omitempty" hcl:"bootstrap" mapstructure:"bootstrap"`
- BootstrapExpect *int `json:"bootstrap_expect,omitempty" hcl:"bootstrap_expect" mapstructure:"bootstrap_expect"`
- Cache Cache `json:"cache,omitempty" hcl:"cache" mapstructure:"cache"`
- CAFile *string `json:"ca_file,omitempty" hcl:"ca_file" mapstructure:"ca_file"`
- CAPath *string `json:"ca_path,omitempty" hcl:"ca_path" mapstructure:"ca_path"`
- CertFile *string `json:"cert_file,omitempty" hcl:"cert_file" mapstructure:"cert_file"`
- Check *CheckDefinition `json:"check,omitempty" hcl:"check" mapstructure:"check"` // needs to be a pointer to avoid partial merges
- CheckOutputMaxSize *int `json:"check_output_max_size,omitempty" hcl:"check_output_max_size" mapstructure:"check_output_max_size"`
- CheckUpdateInterval *string `json:"check_update_interval,omitempty" hcl:"check_update_interval" mapstructure:"check_update_interval"`
- Checks []CheckDefinition `json:"checks,omitempty" hcl:"checks" mapstructure:"checks"`
- ClientAddr *string `json:"client_addr,omitempty" hcl:"client_addr" mapstructure:"client_addr"`
- ConfigEntries ConfigEntries `json:"config_entries,omitempty" hcl:"config_entries" mapstructure:"config_entries"`
- AutoEncrypt AutoEncrypt `json:"auto_encrypt,omitempty" hcl:"auto_encrypt" mapstructure:"auto_encrypt"`
- Connect Connect `json:"connect,omitempty" hcl:"connect" mapstructure:"connect"`
- DNS DNS `json:"dns_config,omitempty" hcl:"dns_config" mapstructure:"dns_config"`
- DNSDomain *string `json:"domain,omitempty" hcl:"domain" mapstructure:"domain"`
- DNSAltDomain *string `json:"alt_domain,omitempty" hcl:"alt_domain" mapstructure:"alt_domain"`
- DNSRecursors []string `json:"recursors,omitempty" hcl:"recursors" mapstructure:"recursors"`
- DataDir *string `json:"data_dir,omitempty" hcl:"data_dir" mapstructure:"data_dir"`
- Datacenter *string `json:"datacenter,omitempty" hcl:"datacenter" mapstructure:"datacenter"`
- DefaultQueryTime *string `json:"default_query_time,omitempty" hcl:"default_query_time" mapstructure:"default_query_time"`
- DisableAnonymousSignature *bool `json:"disable_anonymous_signature,omitempty" hcl:"disable_anonymous_signature" mapstructure:"disable_anonymous_signature"`
- DisableCoordinates *bool `json:"disable_coordinates,omitempty" hcl:"disable_coordinates" mapstructure:"disable_coordinates"`
- DisableHostNodeID *bool `json:"disable_host_node_id,omitempty" hcl:"disable_host_node_id" mapstructure:"disable_host_node_id"`
- DisableHTTPUnprintableCharFilter *bool `json:"disable_http_unprintable_char_filter,omitempty" hcl:"disable_http_unprintable_char_filter" mapstructure:"disable_http_unprintable_char_filter"`
- DisableKeyringFile *bool `json:"disable_keyring_file,omitempty" hcl:"disable_keyring_file" mapstructure:"disable_keyring_file"`
- DisableRemoteExec *bool `json:"disable_remote_exec,omitempty" hcl:"disable_remote_exec" mapstructure:"disable_remote_exec"`
- DisableUpdateCheck *bool `json:"disable_update_check,omitempty" hcl:"disable_update_check" mapstructure:"disable_update_check"`
- DiscardCheckOutput *bool `json:"discard_check_output" hcl:"discard_check_output" mapstructure:"discard_check_output"`
- DiscoveryMaxStale *string `json:"discovery_max_stale" hcl:"discovery_max_stale" mapstructure:"discovery_max_stale"`
- EnableACLReplication *bool `json:"enable_acl_replication,omitempty" hcl:"enable_acl_replication" mapstructure:"enable_acl_replication"`
- EnableAgentTLSForChecks *bool `json:"enable_agent_tls_for_checks,omitempty" hcl:"enable_agent_tls_for_checks" mapstructure:"enable_agent_tls_for_checks"`
- EnableCentralServiceConfig *bool `json:"enable_central_service_config,omitempty" hcl:"enable_central_service_config" mapstructure:"enable_central_service_config"`
- EnableDebug *bool `json:"enable_debug,omitempty" hcl:"enable_debug" mapstructure:"enable_debug"`
- EnableScriptChecks *bool `json:"enable_script_checks,omitempty" hcl:"enable_script_checks" mapstructure:"enable_script_checks"`
- EnableLocalScriptChecks *bool `json:"enable_local_script_checks,omitempty" hcl:"enable_local_script_checks" mapstructure:"enable_local_script_checks"`
- EnableSyslog *bool `json:"enable_syslog,omitempty" hcl:"enable_syslog" mapstructure:"enable_syslog"`
- EncryptKey *string `json:"encrypt,omitempty" hcl:"encrypt" mapstructure:"encrypt"`
- EncryptVerifyIncoming *bool `json:"encrypt_verify_incoming,omitempty" hcl:"encrypt_verify_incoming" mapstructure:"encrypt_verify_incoming"`
- EncryptVerifyOutgoing *bool `json:"encrypt_verify_outgoing,omitempty" hcl:"encrypt_verify_outgoing" mapstructure:"encrypt_verify_outgoing"`
- GossipLAN GossipLANConfig `json:"gossip_lan,omitempty" hcl:"gossip_lan" mapstructure:"gossip_lan"`
- GossipWAN GossipWANConfig `json:"gossip_wan,omitempty" hcl:"gossip_wan" mapstructure:"gossip_wan"`
- HTTPConfig HTTPConfig `json:"http_config,omitempty" hcl:"http_config" mapstructure:"http_config"`
- KeyFile *string `json:"key_file,omitempty" hcl:"key_file" mapstructure:"key_file"`
- LeaveOnTerm *bool `json:"leave_on_terminate,omitempty" hcl:"leave_on_terminate" mapstructure:"leave_on_terminate"`
- Limits Limits `json:"limits,omitempty" hcl:"limits" mapstructure:"limits"`
- LogLevel *string `json:"log_level,omitempty" hcl:"log_level" mapstructure:"log_level"`
- LogJSON *bool `json:"log_json,omitempty" hcl:"log_json" mapstructure:"log_json"`
- LogFile *string `json:"log_file,omitempty" hcl:"log_file" mapstructure:"log_file"`
- LogRotateDuration *string `json:"log_rotate_duration,omitempty" hcl:"log_rotate_duration" mapstructure:"log_rotate_duration"`
- LogRotateBytes *int `json:"log_rotate_bytes,omitempty" hcl:"log_rotate_bytes" mapstructure:"log_rotate_bytes"`
- LogRotateMaxFiles *int `json:"log_rotate_max_files,omitempty" hcl:"log_rotate_max_files" mapstructure:"log_rotate_max_files"`
- MaxQueryTime *string `json:"max_query_time,omitempty" hcl:"max_query_time" mapstructure:"max_query_time"`
- NodeID *string `json:"node_id,omitempty" hcl:"node_id" mapstructure:"node_id"`
- NodeMeta map[string]string `json:"node_meta,omitempty" hcl:"node_meta" mapstructure:"node_meta"`
- NodeName *string `json:"node_name,omitempty" hcl:"node_name" mapstructure:"node_name"`
- Performance Performance `json:"performance,omitempty" hcl:"performance" mapstructure:"performance"`
- PidFile *string `json:"pid_file,omitempty" hcl:"pid_file" mapstructure:"pid_file"`
- Ports Ports `json:"ports,omitempty" hcl:"ports" mapstructure:"ports"`
- PrimaryDatacenter *string `json:"primary_datacenter,omitempty" hcl:"primary_datacenter" mapstructure:"primary_datacenter"`
- PrimaryGateways []string `json:"primary_gateways" hcl:"primary_gateways" mapstructure:"primary_gateways"`
- PrimaryGatewaysInterval *string `json:"primary_gateways_interval,omitempty" hcl:"primary_gateways_interval" mapstructure:"primary_gateways_interval"`
- RPCProtocol *int `json:"protocol,omitempty" hcl:"protocol" mapstructure:"protocol"`
- RaftProtocol *int `json:"raft_protocol,omitempty" hcl:"raft_protocol" mapstructure:"raft_protocol"`
- RaftSnapshotThreshold *int `json:"raft_snapshot_threshold,omitempty" hcl:"raft_snapshot_threshold" mapstructure:"raft_snapshot_threshold"`
- RaftSnapshotInterval *string `json:"raft_snapshot_interval,omitempty" hcl:"raft_snapshot_interval" mapstructure:"raft_snapshot_interval"`
- RaftTrailingLogs *int `json:"raft_trailing_logs,omitempty" hcl:"raft_trailing_logs" mapstructure:"raft_trailing_logs"`
- ReconnectTimeoutLAN *string `json:"reconnect_timeout,omitempty" hcl:"reconnect_timeout" mapstructure:"reconnect_timeout"`
- ReconnectTimeoutWAN *string `json:"reconnect_timeout_wan,omitempty" hcl:"reconnect_timeout_wan" mapstructure:"reconnect_timeout_wan"`
- RejoinAfterLeave *bool `json:"rejoin_after_leave,omitempty" hcl:"rejoin_after_leave" mapstructure:"rejoin_after_leave"`
- RetryJoinIntervalLAN *string `json:"retry_interval,omitempty" hcl:"retry_interval" mapstructure:"retry_interval"`
- RetryJoinIntervalWAN *string `json:"retry_interval_wan,omitempty" hcl:"retry_interval_wan" mapstructure:"retry_interval_wan"`
- RetryJoinLAN []string `json:"retry_join,omitempty" hcl:"retry_join" mapstructure:"retry_join"`
- RetryJoinMaxAttemptsLAN *int `json:"retry_max,omitempty" hcl:"retry_max" mapstructure:"retry_max"`
- RetryJoinMaxAttemptsWAN *int `json:"retry_max_wan,omitempty" hcl:"retry_max_wan" mapstructure:"retry_max_wan"`
- RetryJoinWAN []string `json:"retry_join_wan,omitempty" hcl:"retry_join_wan" mapstructure:"retry_join_wan"`
- SerfAllowedCIDRsLAN []string `json:"serf_lan_allowed_cidrs,omitempty" hcl:"serf_lan_allowed_cidrs" mapstructure:"serf_lan_allowed_cidrs"`
- SerfAllowedCIDRsWAN []string `json:"serf_wan_allowed_cidrs,omitempty" hcl:"serf_wan_allowed_cidrs" mapstructure:"serf_wan_allowed_cidrs"`
- SerfBindAddrLAN *string `json:"serf_lan,omitempty" hcl:"serf_lan" mapstructure:"serf_lan"`
- SerfBindAddrWAN *string `json:"serf_wan,omitempty" hcl:"serf_wan" mapstructure:"serf_wan"`
- ServerMode *bool `json:"server,omitempty" hcl:"server" mapstructure:"server"`
- ServerName *string `json:"server_name,omitempty" hcl:"server_name" mapstructure:"server_name"`
- Service *ServiceDefinition `json:"service,omitempty" hcl:"service" mapstructure:"service"`
- Services []ServiceDefinition `json:"services,omitempty" hcl:"services" mapstructure:"services"`
- SessionTTLMin *string `json:"session_ttl_min,omitempty" hcl:"session_ttl_min" mapstructure:"session_ttl_min"`
- SkipLeaveOnInt *bool `json:"skip_leave_on_interrupt,omitempty" hcl:"skip_leave_on_interrupt" mapstructure:"skip_leave_on_interrupt"`
- StartJoinAddrsLAN []string `json:"start_join,omitempty" hcl:"start_join" mapstructure:"start_join"`
- StartJoinAddrsWAN []string `json:"start_join_wan,omitempty" hcl:"start_join_wan" mapstructure:"start_join_wan"`
- SyslogFacility *string `json:"syslog_facility,omitempty" hcl:"syslog_facility" mapstructure:"syslog_facility"`
- TLSCipherSuites *string `json:"tls_cipher_suites,omitempty" hcl:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
- TLSMinVersion *string `json:"tls_min_version,omitempty" hcl:"tls_min_version" mapstructure:"tls_min_version"`
- TLSPreferServerCipherSuites *bool `json:"tls_prefer_server_cipher_suites,omitempty" hcl:"tls_prefer_server_cipher_suites" mapstructure:"tls_prefer_server_cipher_suites"`
- TaggedAddresses map[string]string `json:"tagged_addresses,omitempty" hcl:"tagged_addresses" mapstructure:"tagged_addresses"`
- Telemetry Telemetry `json:"telemetry,omitempty" hcl:"telemetry" mapstructure:"telemetry"`
- TranslateWANAddrs *bool `json:"translate_wan_addrs,omitempty" hcl:"translate_wan_addrs" mapstructure:"translate_wan_addrs"`
- UI *bool `json:"ui,omitempty" hcl:"ui" mapstructure:"ui"`
- UIContentPath *string `json:"ui_content_path,omitempty" hcl:"ui_content_path" mapstructure:"ui_content_path"`
- UIDir *string `json:"ui_dir,omitempty" hcl:"ui_dir" mapstructure:"ui_dir"`
- UnixSocket UnixSocket `json:"unix_sockets,omitempty" hcl:"unix_sockets" mapstructure:"unix_sockets"`
- VerifyIncoming *bool `json:"verify_incoming,omitempty" hcl:"verify_incoming" mapstructure:"verify_incoming"`
- VerifyIncomingHTTPS *bool `json:"verify_incoming_https,omitempty" hcl:"verify_incoming_https" mapstructure:"verify_incoming_https"`
- VerifyIncomingRPC *bool `json:"verify_incoming_rpc,omitempty" hcl:"verify_incoming_rpc" mapstructure:"verify_incoming_rpc"`
- VerifyOutgoing *bool `json:"verify_outgoing,omitempty" hcl:"verify_outgoing" mapstructure:"verify_outgoing"`
- VerifyServerHostname *bool `json:"verify_server_hostname,omitempty" hcl:"verify_server_hostname" mapstructure:"verify_server_hostname"`
- Watches []map[string]interface{} `json:"watches,omitempty" hcl:"watches" mapstructure:"watches"`
+ ACLToken *string `json:"acl_token,omitempty" hcl:"acl_token" mapstructure:"acl_token"`
+ ACL ACL `json:"acl,omitempty" hcl:"acl" mapstructure:"acl"`
+ Addresses Addresses `json:"addresses,omitempty" hcl:"addresses" mapstructure:"addresses"`
+ AdvertiseAddrLAN *string `json:"advertise_addr,omitempty" hcl:"advertise_addr" mapstructure:"advertise_addr"`
+ AdvertiseAddrLANIPv4 *string `json:"advertise_addr_ipv4,omitempty" hcl:"advertise_addr_ipv4" mapstructure:"advertise_addr_ipv4"`
+ AdvertiseAddrLANIPv6 *string `json:"advertise_addr_ipv6,omitempty" hcl:"advertise_addr_ipv6" mapstructure:"advertise_addr_ipv6"`
+ AdvertiseAddrWAN *string `json:"advertise_addr_wan,omitempty" hcl:"advertise_addr_wan" mapstructure:"advertise_addr_wan"`
+ AdvertiseAddrWANIPv4 *string `json:"advertise_addr_wan_ipv4,omitempty" hcl:"advertise_addr_wan_ipv4" mapstructure:"advertise_addr_wan_ipv4"`
+ AdvertiseAddrWANIPv6 *string `json:"advertise_addr_wan_ipv6,omitempty" hcl:"advertise_addr_wan_ipv6" mapstructure:"advertise_addr_ipv6"`
+ AutoConfig AutoConfigRaw `json:"auto_config,omitempty" hcl:"auto_config" mapstructure:"auto_config"`
+ Autopilot Autopilot `json:"autopilot,omitempty" hcl:"autopilot" mapstructure:"autopilot"`
+ BindAddr *string `json:"bind_addr,omitempty" hcl:"bind_addr" mapstructure:"bind_addr"`
+ Bootstrap *bool `json:"bootstrap,omitempty" hcl:"bootstrap" mapstructure:"bootstrap"`
+ BootstrapExpect *int `json:"bootstrap_expect,omitempty" hcl:"bootstrap_expect" mapstructure:"bootstrap_expect"`
+ Cache Cache `json:"cache,omitempty" hcl:"cache" mapstructure:"cache"`
+ CAFile *string `json:"ca_file,omitempty" hcl:"ca_file" mapstructure:"ca_file"`
+ CAPath *string `json:"ca_path,omitempty" hcl:"ca_path" mapstructure:"ca_path"`
+ CertFile *string `json:"cert_file,omitempty" hcl:"cert_file" mapstructure:"cert_file"`
+ Check *CheckDefinition `json:"check,omitempty" hcl:"check" mapstructure:"check"` // needs to be a pointer to avoid partial merges
+ CheckOutputMaxSize *int `json:"check_output_max_size,omitempty" hcl:"check_output_max_size" mapstructure:"check_output_max_size"`
+ CheckUpdateInterval *string `json:"check_update_interval,omitempty" hcl:"check_update_interval" mapstructure:"check_update_interval"`
+ Checks []CheckDefinition `json:"checks,omitempty" hcl:"checks" mapstructure:"checks"`
+ ClientAddr *string `json:"client_addr,omitempty" hcl:"client_addr" mapstructure:"client_addr"`
+ ConfigEntries ConfigEntries `json:"config_entries,omitempty" hcl:"config_entries" mapstructure:"config_entries"`
+ AutoEncrypt AutoEncrypt `json:"auto_encrypt,omitempty" hcl:"auto_encrypt" mapstructure:"auto_encrypt"`
+ Connect Connect `json:"connect,omitempty" hcl:"connect" mapstructure:"connect"`
+ DNS DNS `json:"dns_config,omitempty" hcl:"dns_config" mapstructure:"dns_config"`
+ DNSDomain *string `json:"domain,omitempty" hcl:"domain" mapstructure:"domain"`
+ DNSAltDomain *string `json:"alt_domain,omitempty" hcl:"alt_domain" mapstructure:"alt_domain"`
+ DNSRecursors []string `json:"recursors,omitempty" hcl:"recursors" mapstructure:"recursors"`
+ DataDir *string `json:"data_dir,omitempty" hcl:"data_dir" mapstructure:"data_dir"`
+ Datacenter *string `json:"datacenter,omitempty" hcl:"datacenter" mapstructure:"datacenter"`
+ DefaultQueryTime *string `json:"default_query_time,omitempty" hcl:"default_query_time" mapstructure:"default_query_time"`
+ DisableAnonymousSignature *bool `json:"disable_anonymous_signature,omitempty" hcl:"disable_anonymous_signature" mapstructure:"disable_anonymous_signature"`
+ DisableCoordinates *bool `json:"disable_coordinates,omitempty" hcl:"disable_coordinates" mapstructure:"disable_coordinates"`
+ DisableHostNodeID *bool `json:"disable_host_node_id,omitempty" hcl:"disable_host_node_id" mapstructure:"disable_host_node_id"`
+ DisableHTTPUnprintableCharFilter *bool `json:"disable_http_unprintable_char_filter,omitempty" hcl:"disable_http_unprintable_char_filter" mapstructure:"disable_http_unprintable_char_filter"`
+ DisableKeyringFile *bool `json:"disable_keyring_file,omitempty" hcl:"disable_keyring_file" mapstructure:"disable_keyring_file"`
+ DisableRemoteExec *bool `json:"disable_remote_exec,omitempty" hcl:"disable_remote_exec" mapstructure:"disable_remote_exec"`
+ DisableUpdateCheck *bool `json:"disable_update_check,omitempty" hcl:"disable_update_check" mapstructure:"disable_update_check"`
+ DiscardCheckOutput *bool `json:"discard_check_output" hcl:"discard_check_output" mapstructure:"discard_check_output"`
+ DiscoveryMaxStale *string `json:"discovery_max_stale" hcl:"discovery_max_stale" mapstructure:"discovery_max_stale"`
+ EnableACLReplication *bool `json:"enable_acl_replication,omitempty" hcl:"enable_acl_replication" mapstructure:"enable_acl_replication"`
+ EnableAgentTLSForChecks *bool `json:"enable_agent_tls_for_checks,omitempty" hcl:"enable_agent_tls_for_checks" mapstructure:"enable_agent_tls_for_checks"`
+ EnableCentralServiceConfig *bool `json:"enable_central_service_config,omitempty" hcl:"enable_central_service_config" mapstructure:"enable_central_service_config"`
+ EnableDebug *bool `json:"enable_debug,omitempty" hcl:"enable_debug" mapstructure:"enable_debug"`
+ EnableScriptChecks *bool `json:"enable_script_checks,omitempty" hcl:"enable_script_checks" mapstructure:"enable_script_checks"`
+ EnableLocalScriptChecks *bool `json:"enable_local_script_checks,omitempty" hcl:"enable_local_script_checks" mapstructure:"enable_local_script_checks"`
+ EnableSyslog *bool `json:"enable_syslog,omitempty" hcl:"enable_syslog" mapstructure:"enable_syslog"`
+ EncryptKey *string `json:"encrypt,omitempty" hcl:"encrypt" mapstructure:"encrypt"`
+ EncryptVerifyIncoming *bool `json:"encrypt_verify_incoming,omitempty" hcl:"encrypt_verify_incoming" mapstructure:"encrypt_verify_incoming"`
+ EncryptVerifyOutgoing *bool `json:"encrypt_verify_outgoing,omitempty" hcl:"encrypt_verify_outgoing" mapstructure:"encrypt_verify_outgoing"`
+ GossipLAN GossipLANConfig `json:"gossip_lan,omitempty" hcl:"gossip_lan" mapstructure:"gossip_lan"`
+ GossipWAN GossipWANConfig `json:"gossip_wan,omitempty" hcl:"gossip_wan" mapstructure:"gossip_wan"`
+ HTTPConfig HTTPConfig `json:"http_config,omitempty" hcl:"http_config" mapstructure:"http_config"`
+ KeyFile *string `json:"key_file,omitempty" hcl:"key_file" mapstructure:"key_file"`
+ LeaveOnTerm *bool `json:"leave_on_terminate,omitempty" hcl:"leave_on_terminate" mapstructure:"leave_on_terminate"`
+ Limits Limits `json:"limits,omitempty" hcl:"limits" mapstructure:"limits"`
+ LogLevel *string `json:"log_level,omitempty" hcl:"log_level" mapstructure:"log_level"`
+ LogJSON *bool `json:"log_json,omitempty" hcl:"log_json" mapstructure:"log_json"`
+ LogFile *string `json:"log_file,omitempty" hcl:"log_file" mapstructure:"log_file"`
+ LogRotateDuration *string `json:"log_rotate_duration,omitempty" hcl:"log_rotate_duration" mapstructure:"log_rotate_duration"`
+ LogRotateBytes *int `json:"log_rotate_bytes,omitempty" hcl:"log_rotate_bytes" mapstructure:"log_rotate_bytes"`
+ LogRotateMaxFiles *int `json:"log_rotate_max_files,omitempty" hcl:"log_rotate_max_files" mapstructure:"log_rotate_max_files"`
+ MaxQueryTime *string `json:"max_query_time,omitempty" hcl:"max_query_time" mapstructure:"max_query_time"`
+ NodeID *string `json:"node_id,omitempty" hcl:"node_id" mapstructure:"node_id"`
+ NodeMeta map[string]string `json:"node_meta,omitempty" hcl:"node_meta" mapstructure:"node_meta"`
+ NodeName *string `json:"node_name,omitempty" hcl:"node_name" mapstructure:"node_name"`
+ Performance Performance `json:"performance,omitempty" hcl:"performance" mapstructure:"performance"`
+ PidFile *string `json:"pid_file,omitempty" hcl:"pid_file" mapstructure:"pid_file"`
+ Ports Ports `json:"ports,omitempty" hcl:"ports" mapstructure:"ports"`
+ PrimaryDatacenter *string `json:"primary_datacenter,omitempty" hcl:"primary_datacenter" mapstructure:"primary_datacenter"`
+ PrimaryGateways []string `json:"primary_gateways" hcl:"primary_gateways" mapstructure:"primary_gateways"`
+ PrimaryGatewaysInterval *string `json:"primary_gateways_interval,omitempty" hcl:"primary_gateways_interval" mapstructure:"primary_gateways_interval"`
+ RPCProtocol *int `json:"protocol,omitempty" hcl:"protocol" mapstructure:"protocol"`
+ RaftProtocol *int `json:"raft_protocol,omitempty" hcl:"raft_protocol" mapstructure:"raft_protocol"`
+ RaftSnapshotThreshold *int `json:"raft_snapshot_threshold,omitempty" hcl:"raft_snapshot_threshold" mapstructure:"raft_snapshot_threshold"`
+ RaftSnapshotInterval *string `json:"raft_snapshot_interval,omitempty" hcl:"raft_snapshot_interval" mapstructure:"raft_snapshot_interval"`
+ RaftTrailingLogs *int `json:"raft_trailing_logs,omitempty" hcl:"raft_trailing_logs" mapstructure:"raft_trailing_logs"`
+ ReconnectTimeoutLAN *string `json:"reconnect_timeout,omitempty" hcl:"reconnect_timeout" mapstructure:"reconnect_timeout"`
+ ReconnectTimeoutWAN *string `json:"reconnect_timeout_wan,omitempty" hcl:"reconnect_timeout_wan" mapstructure:"reconnect_timeout_wan"`
+ RejoinAfterLeave *bool `json:"rejoin_after_leave,omitempty" hcl:"rejoin_after_leave" mapstructure:"rejoin_after_leave"`
+ RetryJoinIntervalLAN *string `json:"retry_interval,omitempty" hcl:"retry_interval" mapstructure:"retry_interval"`
+ RetryJoinIntervalWAN *string `json:"retry_interval_wan,omitempty" hcl:"retry_interval_wan" mapstructure:"retry_interval_wan"`
+ RetryJoinLAN []string `json:"retry_join,omitempty" hcl:"retry_join" mapstructure:"retry_join"`
+ RetryJoinMaxAttemptsLAN *int `json:"retry_max,omitempty" hcl:"retry_max" mapstructure:"retry_max"`
+ RetryJoinMaxAttemptsWAN *int `json:"retry_max_wan,omitempty" hcl:"retry_max_wan" mapstructure:"retry_max_wan"`
+ RetryJoinWAN []string `json:"retry_join_wan,omitempty" hcl:"retry_join_wan" mapstructure:"retry_join_wan"`
+ SerfAllowedCIDRsLAN []string `json:"serf_lan_allowed_cidrs,omitempty" hcl:"serf_lan_allowed_cidrs" mapstructure:"serf_lan_allowed_cidrs"`
+ SerfAllowedCIDRsWAN []string `json:"serf_wan_allowed_cidrs,omitempty" hcl:"serf_wan_allowed_cidrs" mapstructure:"serf_wan_allowed_cidrs"`
+ SerfBindAddrLAN *string `json:"serf_lan,omitempty" hcl:"serf_lan" mapstructure:"serf_lan"`
+ SerfBindAddrWAN *string `json:"serf_wan,omitempty" hcl:"serf_wan" mapstructure:"serf_wan"`
+ ServerMode *bool `json:"server,omitempty" hcl:"server" mapstructure:"server"`
+ ServerName *string `json:"server_name,omitempty" hcl:"server_name" mapstructure:"server_name"`
+ Service *ServiceDefinition `json:"service,omitempty" hcl:"service" mapstructure:"service"`
+ Services []ServiceDefinition `json:"services,omitempty" hcl:"services" mapstructure:"services"`
+ SessionTTLMin *string `json:"session_ttl_min,omitempty" hcl:"session_ttl_min" mapstructure:"session_ttl_min"`
+ SkipLeaveOnInt *bool `json:"skip_leave_on_interrupt,omitempty" hcl:"skip_leave_on_interrupt" mapstructure:"skip_leave_on_interrupt"`
+ StartJoinAddrsLAN []string `json:"start_join,omitempty" hcl:"start_join" mapstructure:"start_join"`
+ StartJoinAddrsWAN []string `json:"start_join_wan,omitempty" hcl:"start_join_wan" mapstructure:"start_join_wan"`
+ SyslogFacility *string `json:"syslog_facility,omitempty" hcl:"syslog_facility" mapstructure:"syslog_facility"`
+ TLSCipherSuites *string `json:"tls_cipher_suites,omitempty" hcl:"tls_cipher_suites" mapstructure:"tls_cipher_suites"`
+ TLSMinVersion *string `json:"tls_min_version,omitempty" hcl:"tls_min_version" mapstructure:"tls_min_version"`
+ TLSPreferServerCipherSuites *bool `json:"tls_prefer_server_cipher_suites,omitempty" hcl:"tls_prefer_server_cipher_suites" mapstructure:"tls_prefer_server_cipher_suites"`
+ TaggedAddresses map[string]string `json:"tagged_addresses,omitempty" hcl:"tagged_addresses" mapstructure:"tagged_addresses"`
+ Telemetry Telemetry `json:"telemetry,omitempty" hcl:"telemetry" mapstructure:"telemetry"`
+ TranslateWANAddrs *bool `json:"translate_wan_addrs,omitempty" hcl:"translate_wan_addrs" mapstructure:"translate_wan_addrs"`
+
+ // DEPRECATED (ui-config) - moved to the ui_config stanza
+ UI *bool `json:"ui,omitempty" hcl:"ui" mapstructure:"ui"`
+ // DEPRECATED (ui-config) - moved to the ui_config stanza
+ UIContentPath *string `json:"ui_content_path,omitempty" hcl:"ui_content_path" mapstructure:"ui_content_path"`
+ // DEPRECATED (ui-config) - moved to the ui_config stanza
+ UIDir *string `json:"ui_dir,omitempty" hcl:"ui_dir" mapstructure:"ui_dir"`
+ UIConfig RawUIConfig `json:"ui_config,omitempty" hcl:"ui_config" mapstructure:"ui_config"`
+
+ UnixSocket UnixSocket `json:"unix_sockets,omitempty" hcl:"unix_sockets" mapstructure:"unix_sockets"`
+ VerifyIncoming *bool `json:"verify_incoming,omitempty" hcl:"verify_incoming" mapstructure:"verify_incoming"`
+ VerifyIncomingHTTPS *bool `json:"verify_incoming_https,omitempty" hcl:"verify_incoming_https" mapstructure:"verify_incoming_https"`
+ VerifyIncomingRPC *bool `json:"verify_incoming_rpc,omitempty" hcl:"verify_incoming_rpc" mapstructure:"verify_incoming_rpc"`
+ VerifyOutgoing *bool `json:"verify_outgoing,omitempty" hcl:"verify_outgoing" mapstructure:"verify_outgoing"`
+ VerifyServerHostname *bool `json:"verify_server_hostname,omitempty" hcl:"verify_server_hostname" mapstructure:"verify_server_hostname"`
+ Watches []map[string]interface{} `json:"watches,omitempty" hcl:"watches" mapstructure:"watches"`
// This isn't used by Consul but we've documented a feature where users
// can deploy their snapshot agent configs alongside their Consul configs
@@ -769,3 +775,24 @@ type AutoConfigAuthorizerRaw struct {
NotBeforeLeeway *string `json:"not_before_leeway,omitempty" hcl:"not_before_leeway" mapstructure:"not_before_leeway"`
ClockSkewLeeway *string `json:"clock_skew_leeway,omitempty" hcl:"clock_skew_leeway" mapstructure:"clock_skew_leeway"`
}
+
+type RawUIConfig struct {
+ Enabled *bool `json:"enabled,omitempty" hcl:"enabled" mapstructure:"enabled"`
+ Dir *string `json:"dir,omitempty" hcl:"dir" mapstructure:"dir"`
+ ContentPath *string `json:"content_path,omitempty" hcl:"content_path" mapstructure:"content_path"`
+ MetricsProvider *string `json:"metrics_provider,omitempty" hcl:"metrics_provider" mapstructure:"metrics_provider"`
+ MetricsProviderFiles []string `json:"metrics_provider_files,omitempty" hcl:"metrics_provider_files" mapstructure:"metrics_provider_files"`
+ MetricsProviderOptionsJSON *string `json:"metrics_provider_options_json,omitempty" hcl:"metrics_provider_options_json" mapstructure:"metrics_provider_options_json"`
+ MetricsProxy RawUIMetricsProxy `json:"metrics_proxy,omitempty" hcl:"metrics_proxy" mapstructure:"metrics_proxy"`
+ DashboardURLTemplates map[string]string `json:"dashboard_url_templates" hcl:"dashboard_url_templates" mapstructure:"dashboard_url_templates"`
+}
+
+type RawUIMetricsProxy struct {
+ BaseURL *string `json:"base_url,omitempty" hcl:"base_url" mapstructure:"base_url"`
+ AddHeaders []RawUIMetricsProxyAddHeader `json:"add_headers,omitempty" hcl:"add_headers" mapstructure:"add_headers"`
+}
+
+type RawUIMetricsProxyAddHeader struct {
+ Name *string `json:"name,omitempty" hcl:"name" mapstructure:"name"`
+ Value *string `json:"value,omitempty" hcl:"value" mapstructure:"value"`
+}
diff --git a/agent/config/default.go b/agent/config/default.go
index 632c96414a..1fac15381e 100644
--- a/agent/config/default.go
+++ b/agent/config/default.go
@@ -140,7 +140,9 @@ func DevSource() Source {
disable_anonymous_signature = true
disable_keyring_file = true
enable_debug = true
- ui = true
+ ui_config {
+ enabled = true
+ }
log_level = "DEBUG"
server = true
diff --git a/agent/config/flags.go b/agent/config/flags.go
index 8cc596a499..a032944d3e 100644
--- a/agent/config/flags.go
+++ b/agent/config/flags.go
@@ -111,8 +111,8 @@ func AddFlags(fs *flag.FlagSet, f *BuilderOpts) {
add(&f.Config.Ports.SerfWAN, "serf-wan-port", "Sets the Serf WAN port to listen on.")
add(&f.Config.ServerMode, "server", "Switches agent to server mode.")
add(&f.Config.EnableSyslog, "syslog", "Enables logging to syslog.")
- add(&f.Config.UI, "ui", "Enables the built-in static web UI server.")
- add(&f.Config.UIContentPath, "ui-content-path", "Sets the external UI path to a string. Defaults to: /ui/ ")
- add(&f.Config.UIDir, "ui-dir", "Path to directory containing the web UI resources.")
+ add(&f.Config.UIConfig.Enabled, "ui", "Enables the built-in static web UI server.")
+ add(&f.Config.UIConfig.ContentPath, "ui-content-path", "Sets the external UI path to a string. Defaults to: /ui/ ")
+ add(&f.Config.UIConfig.Dir, "ui-dir", "Path to directory containing the web UI resources.")
add(&f.HCL, "hcl", "hcl config fragment. Can be specified multiple times.")
}
diff --git a/agent/config/runtime.go b/agent/config/runtime.go
index 7577854224..34b870c2a0 100644
--- a/agent/config/runtime.go
+++ b/agent/config/runtime.go
@@ -694,13 +694,6 @@ type RuntimeConfig struct {
// flag: -enable-script-checks
EnableRemoteScriptChecks bool
- // EnableUI enables the statically-compiled assets for the Consul web UI and
- // serves them at the default /ui/ endpoint automatically.
- //
- // hcl: enable_ui = (true|false)
- // flag: -ui
- EnableUI bool
-
// EncryptKey contains the encryption key to use for the Serf communication.
//
// hcl: encrypt = string
@@ -1411,16 +1404,18 @@ type RuntimeConfig struct {
// hcl: limits { txn_max_req_len = uint64 }
TxnMaxReqLen uint64
- // UIDir is the directory containing the Web UI resources.
- // If provided, the UI endpoints will be enabled.
+ // UIConfig holds various runtime options that control both the agent's
+ // behavior while serving the UI (e.g. whether it's enabled, what path it's
+ // mounted on) as well as options that enable or disable features within the
+ // UI.
//
- // hcl: ui_dir = string
- // flag: -ui-dir string
- UIDir string
-
- //UIContentPath is a string that sets the external
- // path to a string. Default: /ui/
- UIContentPath string
+ // NOTE: Never read from this field directly once the agent has started up
+ // since the UI config is reloadable. The on in the agent's config field may
+ // be out of date. Use the agent.getUIConfig() method to get the latest config
+ // in a thread-safe way.
+ //
+ // hcl: ui_config { ... }
+ UIConfig UIConfig
// UnixSocketGroup contains the group of the file permissions when
// Consul binds to UNIX sockets.
@@ -1518,6 +1513,27 @@ type AutoConfigAuthorizer struct {
AllowReuse bool
}
+type UIConfig struct {
+ Enabled bool
+ Dir string
+ ContentPath string
+ MetricsProvider string
+ MetricsProviderFiles []string
+ MetricsProviderOptionsJSON string
+ MetricsProxy UIMetricsProxy
+ DashboardURLTemplates map[string]string
+}
+
+type UIMetricsProxy struct {
+ BaseURL string
+ AddHeaders []UIMetricsProxyAddHeader
+}
+
+type UIMetricsProxyAddHeader struct {
+ Name string
+ Value string
+}
+
func (c *RuntimeConfig) apiAddresses(maxPerType int) (unixAddrs, httpAddrs, httpsAddrs []string) {
if len(c.HTTPSAddrs) > 0 {
for i, addr := range c.HTTPSAddrs {
diff --git a/agent/config/runtime_test.go b/agent/config/runtime_test.go
index a16eb6f5bc..a8acf6614a 100644
--- a/agent/config/runtime_test.go
+++ b/agent/config/runtime_test.go
@@ -290,7 +290,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
rt.DisableAnonymousSignature = true
rt.DisableKeyringFile = true
rt.EnableDebug = true
- rt.EnableUI = true
+ rt.UIConfig.Enabled = true
rt.LeaveOnTerm = false
rt.Logging.LogLevel = "DEBUG"
rt.RPCAdvertiseAddr = tcpAddr("127.0.0.1:8300")
@@ -850,7 +850,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
`-data-dir=` + dataDir,
},
patch: func(rt *RuntimeConfig) {
- rt.EnableUI = true
+ rt.UIConfig.Enabled = true
rt.DataDir = dataDir
},
},
@@ -861,7 +861,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
`-data-dir=` + dataDir,
},
patch: func(rt *RuntimeConfig) {
- rt.UIDir = "a"
+ rt.UIConfig.Dir = "a"
rt.DataDir = dataDir
},
},
@@ -873,7 +873,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
},
patch: func(rt *RuntimeConfig) {
- rt.UIContentPath = "/a/b/"
+ rt.UIConfig.ContentPath = "/a/b/"
rt.DataDir = dataDir
},
},
@@ -1712,7 +1712,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
},
json: []string{`{ "acl_datacenter": "%" }`},
hcl: []string{`acl_datacenter = "%"`},
- err: `acl_datacenter cannot be "%". Please use only [a-z0-9-_]`,
+ err: `acl_datacenter can only contain lowercase alphanumeric, - or _ characters.`,
warns: []string{`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`},
},
{
@@ -1881,7 +1881,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
args: []string{`-data-dir=` + dataDir},
json: []string{`{ "datacenter": "%" }`},
hcl: []string{`datacenter = "%"`},
- err: `datacenter cannot be "%". Please use only [a-z0-9-_]`,
+ err: `datacenter can only contain lowercase alphanumeric, - or _ characters.`,
},
{
desc: "dns does not allow socket",
@@ -1894,16 +1894,16 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
err: "DNS address cannot be a unix socket",
},
{
- desc: "ui and ui_dir",
+ desc: "ui enabled and dir specified",
args: []string{
`-datacenter=a`,
`-data-dir=` + dataDir,
},
- json: []string{`{ "ui": true, "ui_dir": "a" }`},
- hcl: []string{`ui = true ui_dir = "a"`},
- err: "Both the ui and ui-dir flags were specified, please provide only one.\n" +
- "If trying to use your own web UI resources, use the ui-dir flag.\n" +
- "The web UI is included in the binary so use ui to enable it",
+ json: []string{`{ "ui_config": { "enabled": true, "dir": "a" } }`},
+ hcl: []string{`ui_config { enabled = true dir = "a"}`},
+ err: "Both the ui_config.enabled and ui_config.dir (or -ui and -ui-dir) were specified, please provide only one.\n" +
+ "If trying to use your own web UI resources, use ui_config.dir or the -ui-dir flag.\n" +
+ "The web UI is included in the binary so use ui_config.enabled or the -ui flag to enable it",
},
// test ANY address failures
@@ -4251,6 +4251,169 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
rt.CertFile = "foo"
},
},
+
+ // UI Config tests
+ {
+ desc: "ui config deprecated",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui": true,
+ "ui_content_path": "/bar"
+ }`},
+ hcl: []string{`
+ ui = true
+ ui_content_path = "/bar"
+ `},
+ warns: []string{
+ `The 'ui' field is deprecated. Use the 'ui_config.enabled' field instead.`,
+ `The 'ui_content_path' field is deprecated. Use the 'ui_config.content_path' field instead.`,
+ },
+ patch: func(rt *RuntimeConfig) {
+ // Should still work!
+ rt.UIConfig.Enabled = true
+ rt.UIConfig.ContentPath = "/bar/"
+ rt.DataDir = dataDir
+ },
+ },
+ {
+ desc: "ui-dir config deprecated",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_dir": "/bar"
+ }`},
+ hcl: []string{`
+ ui_dir = "/bar"
+ `},
+ warns: []string{
+ `The 'ui_dir' field is deprecated. Use the 'ui_config.dir' field instead.`,
+ },
+ patch: func(rt *RuntimeConfig) {
+ // Should still work!
+ rt.UIConfig.Dir = "/bar"
+ rt.DataDir = dataDir
+ },
+ },
+ {
+ desc: "metrics_provider constraint",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "metrics_provider": "((((lisp 4 life))))"
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ metrics_provider = "((((lisp 4 life))))"
+ }
+ `},
+ err: `ui_config.metrics_provider can only contain lowercase alphanumeric, - or _ characters.`,
+ },
+ {
+ desc: "metrics_provider_options_json invalid JSON",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "metrics_provider_options_json": "not valid JSON"
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ metrics_provider_options_json = "not valid JSON"
+ }
+ `},
+ err: `ui_config.metrics_provider_options_json must be empty or a string containing a valid JSON object.`,
+ },
+ {
+ desc: "metrics_provider_options_json not an object",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "metrics_provider_options_json": "1.0"
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ metrics_provider_options_json = "1.0"
+ }
+ `},
+ err: `ui_config.metrics_provider_options_json must be empty or a string containing a valid JSON object.`,
+ },
+ {
+ desc: "metrics_proxy.base_url valid",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "metrics_proxy": {
+ "base_url": "___"
+ }
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ metrics_proxy {
+ base_url = "___"
+ }
+ }
+ `},
+ err: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`,
+ },
+ {
+ desc: "metrics_proxy.base_url http(s)",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "metrics_proxy": {
+ "base_url": "localhost:1234"
+ }
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ metrics_proxy {
+ base_url = "localhost:1234"
+ }
+ }
+ `},
+ err: `ui_config.metrics_proxy.base_url must be a valid http or https URL.`,
+ },
+ {
+ desc: "dashboard_url_templates key format",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "dashboard_url_templates": {
+ "(*&ASDOUISD)": "localhost:1234"
+ }
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ dashboard_url_templates {
+ "(*&ASDOUISD)" = "localhost:1234"
+ }
+ }
+ `},
+ err: `ui_config.dashboard_url_templates key names can only contain lowercase alphanumeric, - or _ characters.`,
+ },
+ {
+ desc: "dashboard_url_templates value format",
+ args: []string{`-data-dir=` + dataDir},
+ json: []string{`{
+ "ui_config": {
+ "dashboard_url_templates": {
+ "services": "localhost:1234"
+ }
+ }
+ }`},
+ hcl: []string{`
+ ui_config {
+ dashboard_url_templates {
+ services = "localhost:1234"
+ }
+ }
+ `},
+ err: `ui_config.dashboard_url_templates values must be a valid http or https URL.`,
+ },
}
testConfig(t, tests, dataDir)
@@ -5070,9 +5233,26 @@ func TestFullConfig(t *testing.T) {
"tls_min_version": "pAOWafkR",
"tls_prefer_server_cipher_suites": true,
"translate_wan_addrs": true,
- "ui": true,
- "ui_dir": "11IFzAUn",
- "ui_content_path": "consul",
+ "ui_config": {
+ "enabled": true,
+ "dir": "pVncV4Ey",
+ "content_path": "qp1WRhYH",
+ "metrics_provider": "sgnaoa_lower_case",
+ "metrics_provider_files": ["sgnaMFoa", "dicnwkTH"],
+ "metrics_provider_options_json": "{\"DIbVQadX\": 1}",
+ "metrics_proxy": {
+ "base_url": "http://foo.bar",
+ "add_headers": [
+ {
+ "name": "p3nynwc9",
+ "value": "TYBgnN2F"
+ }
+ ]
+ },
+ "dashboard_url_templates": {
+ "u2eziu2n_lower_case": "http://lkjasd.otr"
+ }
+ },
"unix_sockets": {
"group": "8pFodrV8",
"mode": "E8sAwOv4",
@@ -5736,9 +5916,26 @@ func TestFullConfig(t *testing.T) {
tls_min_version = "pAOWafkR"
tls_prefer_server_cipher_suites = true
translate_wan_addrs = true
- ui = true
- ui_dir = "11IFzAUn"
- ui_content_path = "consul"
+ ui_config {
+ enabled = true
+ dir = "pVncV4Ey"
+ content_path = "qp1WRhYH"
+ metrics_provider = "sgnaoa_lower_case"
+ metrics_provider_files = ["sgnaMFoa", "dicnwkTH"]
+ metrics_provider_options_json = "{\"DIbVQadX\": 1}"
+ metrics_proxy {
+ base_url = "http://foo.bar"
+ add_headers = [
+ {
+ name = "p3nynwc9"
+ value = "TYBgnN2F"
+ }
+ ]
+ }
+ dashboard_url_templates {
+ u2eziu2n_lower_case = "http://lkjasd.otr"
+ }
+ }
unix_sockets = {
group = "8pFodrV8"
mode = "E8sAwOv4"
@@ -6110,7 +6307,6 @@ func TestFullConfig(t *testing.T) {
EnableDebug: true,
EnableRemoteScriptChecks: true,
EnableLocalScriptChecks: true,
- EnableUI: true,
EncryptKey: "A4wELWqH",
EncryptVerifyIncoming: true,
EncryptVerifyOutgoing: true,
@@ -6507,10 +6703,26 @@ func TestFullConfig(t *testing.T) {
"wan": "78.63.37.19",
"wan_ipv4": "78.63.37.19",
},
- TranslateWANAddrs: true,
- TxnMaxReqLen: 5678000000000000,
- UIContentPath: "/consul/",
- UIDir: "11IFzAUn",
+ TranslateWANAddrs: true,
+ TxnMaxReqLen: 5678000000000000,
+ UIConfig: UIConfig{
+ Enabled: true,
+ Dir: "pVncV4Ey",
+ ContentPath: "/qp1WRhYH/", // slashes are added in parsing
+ MetricsProvider: "sgnaoa_lower_case",
+ MetricsProviderFiles: []string{"sgnaMFoa", "dicnwkTH"},
+ MetricsProviderOptionsJSON: "{\"DIbVQadX\": 1}",
+ MetricsProxy: UIMetricsProxy{
+ BaseURL: "http://foo.bar",
+ AddHeaders: []UIMetricsProxyAddHeader{
+ {
+ Name: "p3nynwc9",
+ Value: "TYBgnN2F",
+ },
+ },
+ },
+ DashboardURLTemplates: map[string]string{"u2eziu2n_lower_case": "http://lkjasd.otr"},
+ },
UnixSocketUser: "E0nB1DwA",
UnixSocketGroup: "8pFodrV8",
UnixSocketMode: "E8sAwOv4",
@@ -6589,7 +6801,7 @@ func TestFullConfig(t *testing.T) {
// we are patching a handful of safe fields to make validation pass.
rt.Bootstrap = false
rt.DevMode = false
- rt.EnableUI = false
+ rt.UIConfig.Enabled = false
rt.SegmentName = ""
rt.Segments = nil
@@ -6599,7 +6811,7 @@ func TestFullConfig(t *testing.T) {
}
// check the warnings
- require.ElementsMatch(t, warns, b.Warnings, "Warnings: %v", b.Warnings)
+ require.ElementsMatch(t, warns, b.Warnings, "Warnings: %#v", b.Warnings)
})
}
}
@@ -7005,7 +7217,6 @@ func TestSanitize(t *testing.T) {
"EnableCentralServiceConfig": false,
"EnableLocalScriptChecks": false,
"EnableRemoteScriptChecks": false,
- "EnableUI": false,
"EncryptKey": "hidden",
"EncryptVerifyIncoming": false,
"EncryptVerifyOutgoing": false,
@@ -7179,8 +7390,19 @@ func TestSanitize(t *testing.T) {
},
"TranslateWANAddrs": false,
"TxnMaxReqLen": 5678000000000000,
- "UIDir": "",
- "UIContentPath": "",
+ "UIConfig": {
+ "ContentPath": "",
+ "Dir": "",
+ "Enabled": false,
+ "MetricsProvider": "",
+ "MetricsProviderFiles": [],
+ "MetricsProviderOptionsJSON": "",
+ "MetricsProxy": {
+ "AddHeaders": [],
+ "BaseURL": ""
+ },
+ "DashboardURLTemplates": {}
+ },
"UnixSocketGroup": "",
"UnixSocketMode": "",
"UnixSocketUser": "",
diff --git a/agent/http.go b/agent/http.go
index 48158ab3c0..43f31f688e 100644
--- a/agent/http.go
+++ b/agent/http.go
@@ -1,16 +1,13 @@
package agent
import (
- "bytes"
"encoding/json"
"fmt"
"io"
- "io/ioutil"
"net"
"net/http"
"net/http/pprof"
"net/url"
- "os"
"reflect"
"regexp"
"strconv"
@@ -21,8 +18,10 @@ import (
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/cache"
+ "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/consul"
"github.com/hashicorp/consul/agent/structs"
+ "github.com/hashicorp/consul/agent/uiserver"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging"
@@ -78,135 +77,12 @@ func (e ForbiddenError) Error() string {
return "Access is restricted"
}
-// HTTPHandlers provides http.Handler functions for the HTTP APi.
+// HTTPHandlers provides an HTTP api for an agent.
type HTTPHandlers struct {
- agent *Agent
- denylist *Denylist
-}
-
-// bufferedFile implements os.File and allows us to modify a file from disk by
-// writing out the new version into a buffer and then serving file reads from
-// that. It assumes you are modifying a real file and presents the actual file's
-// info when queried.
-type bufferedFile struct {
- templated *bytes.Reader
- info os.FileInfo
-}
-
-func newBufferedFile(buf *bytes.Buffer, raw http.File) *bufferedFile {
- info, _ := raw.Stat()
- return &bufferedFile{
- templated: bytes.NewReader(buf.Bytes()),
- info: info,
- }
-}
-
-func (t *bufferedFile) Read(p []byte) (n int, err error) {
- return t.templated.Read(p)
-}
-
-func (t *bufferedFile) Seek(offset int64, whence int) (int64, error) {
- return t.templated.Seek(offset, whence)
-}
-
-func (t *bufferedFile) Close() error {
- return nil
-}
-
-func (t *bufferedFile) Readdir(count int) ([]os.FileInfo, error) {
- return nil, errors.New("not a directory")
-}
-
-func (t *bufferedFile) Stat() (os.FileInfo, error) {
- return t, nil
-}
-
-func (t *bufferedFile) Name() string {
- return t.info.Name()
-}
-
-func (t *bufferedFile) Size() int64 {
- return int64(t.templated.Len())
-}
-
-func (t *bufferedFile) Mode() os.FileMode {
- return t.info.Mode()
-}
-
-func (t *bufferedFile) ModTime() time.Time {
- return t.info.ModTime()
-}
-
-func (t *bufferedFile) IsDir() bool {
- return false
-}
-
-func (t *bufferedFile) Sys() interface{} {
- return nil
-}
-
-type redirectFS struct {
- fs http.FileSystem
-}
-
-func (fs *redirectFS) Open(name string) (http.File, error) {
- file, err := fs.fs.Open(name)
- if err != nil {
- file, err = fs.fs.Open("/index.html")
- }
- return file, err
-}
-
-type settingsInjectedIndexFS struct {
- fs http.FileSystem
- UISettings map[string]interface{}
-}
-
-func (fs *settingsInjectedIndexFS) Open(name string) (http.File, error) {
- file, err := fs.fs.Open(name)
- if err != nil || name != "/index.html" {
- return file, err
- }
-
- content, err := ioutil.ReadAll(file)
- if err != nil {
- return nil, fmt.Errorf("failed reading index.html: %s", err)
- }
- file.Seek(0, 0)
-
- // Replace the placeholder in the meta ENV with the actual UI config settings.
- // Ember passes the ENV with URL encoded JSON in a meta tag. We are replacing
- // a key and value that is the encoded version of
- // `"CONSUL_UI_SETTINGS_PLACEHOLDER":"__CONSUL_UI_SETTINGS_GO_HERE__"`
- // with a URL-encoded JSON blob representing the actual config.
-
- // First built an escaped, JSON blob from the settings passed.
- bs, err := json.Marshal(fs.UISettings)
- if err != nil {
- return nil, fmt.Errorf("failed marshalling UI settings JSON: %s", err)
- }
- // We want to remove the first and last chars which will be the { and } since
- // we are injecting these variabled into the middle of an existing object.
- bs = bytes.Trim(bs, "{}")
-
- // We use PathEscape because we don't want spaces to be turned into "+" like
- // QueryEscape does.
- escaped := url.PathEscape(string(bs))
-
- content = bytes.Replace(content,
- []byte("%22CONSUL_UI_SETTINGS_PLACEHOLDER%22%3A%22__CONSUL_UI_SETTINGS_GO_HERE__%22"),
- []byte(escaped), 1)
-
- // We also need to inject the content path. This used to be a go template
- // hence the syntax but for now simple string replacement is fine esp. since
- // all the other templated stuff above can't easily be done that was as we are
- // replacing an entire placeholder element in an encoded JSON blob with
- // multiple encoded JSON elements.
- if path, ok := fs.UISettings["CONSUL_CONTENT_PATH"].(string); ok {
- content = bytes.Replace(content, []byte("{{.ContentPath}}"), []byte(path), -1)
- }
-
- return newBufferedFile(bytes.NewBuffer(content), file), nil
+ agent *Agent
+ denylist *Denylist
+ configReloaders []ConfigReloader
+ h http.Handler
}
// endpoint is a Consul-specific HTTP handler that takes the usual arguments in
@@ -249,8 +125,45 @@ func (w *wrappedMux) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
w.handler.ServeHTTP(resp, req)
}
-// handler is used to attach our handlers to the mux
+// ReloadConfig updates any internal state when the config is changed at
+// runtime.
+func (s *HTTPHandlers) ReloadConfig(newCfg *config.RuntimeConfig) error {
+ for _, r := range s.configReloaders {
+ if err := r(newCfg); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// handler is used to initialize the Handler. In agent code we only ever call
+// this once during agent initialization so it was always intended as a single
+// pass init method. However many test rely on it as a cheaper way to get a
+// handler to call ServeHTTP against and end up calling it multiple times on a
+// single agent instance. Until this method had to manage state that might be
+// affected by a reload or otherwise vary over time that was not problematic
+// although it was wasteful to redo all this setup work multiple times in one
+// test.
+//
+// Now uiserver and possibly other components need to handle reloadable state
+// having test randomly clobber the state with the original config again for
+// each call gets confusing fast. So handler will memoize it's response - it's
+// allowed to call it multiple times on the same agent, but it will only do the
+// work the first time and return the same handler on subsequent calls.
+//
+// The `enableDebug` argument used in the first call will be effective and a
+// later change will not do anything. The same goes for the initial config. For
+// example if config is reloaded with UI enabled but it was not originally, the
+// http.Handler returned will still have it disabled.
+//
+// The first call must not be concurrent with any other call. Subsequent calls
+// may be concurrent with HTTP requests since no state is modified.
func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
+ // Memoize multiple calls.
+ if s.h != nil {
+ return s.h
+ }
+
mux := http.NewServeMux()
// handleFuncMetrics takes the given pattern and handler and wraps to produce
@@ -347,31 +260,27 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
handlePProf("/debug/pprof/trace", pprof.Trace)
if s.IsUIEnabled() {
- var uifs http.FileSystem
- // Use the custom UI dir if provided.
- if s.agent.config.UIDir != "" {
- uifs = http.Dir(s.agent.config.UIDir)
- } else {
- fs := assetFS()
- uifs = fs
- }
+ // Note that we _don't_ support reloading ui_config.{enabled, content_dir,
+ // content_path} since this only runs at initial startup.
- uifs = &redirectFS{fs: &settingsInjectedIndexFS{fs: uifs, UISettings: s.GetUIENVFromConfig()}}
- // create a http handler using the ui file system
- // and the headers specified by the http_config.response_headers user config
- uifsWithHeaders := serveHandlerWithHeaders(
- http.FileServer(uifs),
+ uiHandler := uiserver.NewHandler(s.agent.config, s.agent.logger.Named(logging.HTTP))
+ s.configReloaders = append(s.configReloaders, uiHandler.ReloadConfig)
+
+ // Wrap it to add the headers specified by the http_config.response_headers
+ // user config
+ uiHandlerWithHeaders := serveHandlerWithHeaders(
+ uiHandler,
s.agent.config.HTTPResponseHeaders,
)
mux.Handle(
"/robots.txt",
- uifsWithHeaders,
+ uiHandlerWithHeaders,
)
mux.Handle(
- s.agent.config.UIContentPath,
+ s.agent.config.UIConfig.ContentPath,
http.StripPrefix(
- s.agent.config.UIContentPath,
- uifsWithHeaders,
+ s.agent.config.UIConfig.ContentPath,
+ uiHandlerWithHeaders,
),
)
}
@@ -384,21 +293,11 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler {
h = mux
}
h = s.enterpriseHandler(h)
- return &wrappedMux{
+ s.h = &wrappedMux{
mux: mux,
handler: h,
}
-}
-
-func (s *HTTPHandlers) GetUIENVFromConfig() map[string]interface{} {
- vars := map[string]interface{}{
- "CONSUL_CONTENT_PATH": s.agent.config.UIContentPath,
- "CONSUL_ACLS_ENABLED": s.agent.config.ACLsEnabled,
- }
-
- s.addEnterpriseUIENVVars(vars)
-
- return vars
+ return s.h
}
// nodeName returns the node name of the agent
@@ -636,11 +535,17 @@ func (s *HTTPHandlers) marshalJSON(req *http.Request, obj interface{}) ([]byte,
// Returns true if the UI is enabled.
func (s *HTTPHandlers) IsUIEnabled() bool {
- return s.agent.config.UIDir != "" || s.agent.config.EnableUI
+ // Note that we _don't_ support reloading ui_config.{enabled,content_dir}
+ // since this only runs at initial startup.
+ return s.agent.config.UIConfig.Dir != "" || s.agent.config.UIConfig.Enabled
}
// Renders a simple index page
func (s *HTTPHandlers) Index(resp http.ResponseWriter, req *http.Request) {
+ // Send special headers too since this endpoint isn't wrapped with something
+ // that sends them.
+ setHeaders(resp, s.agent.config.HTTPResponseHeaders)
+
// Check if this is a non-index path
if req.URL.Path != "/" {
resp.WriteHeader(http.StatusNotFound)
@@ -655,7 +560,12 @@ func (s *HTTPHandlers) Index(resp http.ResponseWriter, req *http.Request) {
}
// Redirect to the UI endpoint
- http.Redirect(resp, req, s.agent.config.UIContentPath, http.StatusMovedPermanently) // 301
+ http.Redirect(
+ resp,
+ req,
+ s.agent.config.UIConfig.ContentPath,
+ http.StatusMovedPermanently,
+ ) // 301
}
func decodeBody(body io.Reader, out interface{}) error {
diff --git a/agent/http_oss.go b/agent/http_oss.go
index e9439d3ef2..42009e8ad7 100644
--- a/agent/http_oss.go
+++ b/agent/http_oss.go
@@ -55,8 +55,6 @@ func (s *HTTPHandlers) rewordUnknownEnterpriseFieldError(err error) error {
return err
}
-func (s *HTTPHandlers) addEnterpriseUIENVVars(_ map[string]interface{}) {}
-
func parseACLAuthMethodEnterpriseMeta(req *http.Request, _ *structs.ACLAuthMethodEnterpriseMeta) error {
if methodNS := req.URL.Query().Get("authmethod-ns"); methodNS != "" {
return BadRequestError{Reason: "Invalid query parameter: \"authmethod-ns\" - Namespaces are a Consul Enterprise feature"}
diff --git a/agent/http_test.go b/agent/http_test.go
index 233ae0e180..883bf70c27 100644
--- a/agent/http_test.go
+++ b/agent/http_test.go
@@ -20,6 +20,7 @@ import (
"time"
"github.com/NYTimes/gziphandler"
+ "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/structs"
tokenStore "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api"
@@ -417,62 +418,56 @@ func TestHTTPAPI_TranslateAddrHeader(t *testing.T) {
func TestHTTPAPIResponseHeaders(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, `
+ ui_config {
+ # Explicitly disable UI so we can ensure the index replacement gets headers too.
+ enabled = false
+ }
http_config {
response_headers = {
"Access-Control-Allow-Origin" = "*"
"X-XSS-Protection" = "1; mode=block"
- }
- }
- `)
- defer a.Shutdown()
-
- resp := httptest.NewRecorder()
- handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
- return nil, nil
- }
-
- req, _ := http.NewRequest("GET", "/v1/agent/self", nil)
- a.srv.wrap(handler, []string{"GET"})(resp, req)
-
- origin := resp.Header().Get("Access-Control-Allow-Origin")
- if origin != "*" {
- t.Fatalf("bad Access-Control-Allow-Origin: expected %q, got %q", "*", origin)
- }
-
- xss := resp.Header().Get("X-XSS-Protection")
- if xss != "1; mode=block" {
- t.Fatalf("bad X-XSS-Protection header: expected %q, got %q", "1; mode=block", xss)
- }
-}
-func TestUIResponseHeaders(t *testing.T) {
- t.Parallel()
- a := NewTestAgent(t, `
- http_config {
- response_headers = {
- "Access-Control-Allow-Origin" = "*"
"X-Frame-Options" = "SAMEORIGIN"
}
}
`)
defer a.Shutdown()
+ requireHasHeadersSet(t, a, "/v1/agent/self")
+
+ // Check the Index page that just renders a simple message with UI disabled
+ // also gets the right headers.
+ requireHasHeadersSet(t, a, "/")
+}
+
+func requireHasHeadersSet(t *testing.T, a *TestAgent, path string) {
+ t.Helper()
+
resp := httptest.NewRecorder()
- handler := func(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
- return nil, nil
- }
+ req, _ := http.NewRequest("GET", path, nil)
+ a.srv.handler(true).ServeHTTP(resp, req)
- req, _ := http.NewRequest("GET", "/ui", nil)
- a.srv.wrap(handler, []string{"GET"})(resp, req)
+ hdrs := resp.Header()
+ require.Equal(t, "*", hdrs.Get("Access-Control-Allow-Origin"),
+ "Access-Control-Allow-Origin header value incorrect")
- origin := resp.Header().Get("Access-Control-Allow-Origin")
- if origin != "*" {
- t.Fatalf("bad Access-Control-Allow-Origin: expected %q, got %q", "*", origin)
- }
+ require.Equal(t, "1; mode=block", hdrs.Get("X-XSS-Protection"),
+ "X-XSS-Protection header value incorrect")
+}
- frameOptions := resp.Header().Get("X-Frame-Options")
- if frameOptions != "SAMEORIGIN" {
- t.Fatalf("bad X-XSS-Protection header: expected %q, got %q", "SAMEORIGIN", frameOptions)
- }
+func TestUIResponseHeaders(t *testing.T) {
+ t.Parallel()
+ a := NewTestAgent(t, `
+ http_config {
+ response_headers = {
+ "Access-Control-Allow-Origin" = "*"
+ "X-XSS-Protection" = "1; mode=block"
+ "X-Frame-Options" = "SAMEORIGIN"
+ }
+ }
+ `)
+ defer a.Shutdown()
+
+ requireHasHeadersSet(t, a, "/ui")
}
func TestAcceptEncodingGzip(t *testing.T) {
@@ -1198,15 +1193,47 @@ func TestACLResolution(t *testing.T) {
func TestEnableWebUI(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, `
- ui = true
+ ui_config {
+ enabled = true
+ }
`)
defer a.Shutdown()
req, _ := http.NewRequest("GET", "/ui/", nil)
resp := httptest.NewRecorder()
a.srv.handler(true).ServeHTTP(resp, req)
- if resp.Code != 200 {
- t.Fatalf("should handle ui")
+ require.Equal(t, http.StatusOK, resp.Code)
+
+ // Validate that it actually sent the index page we expect since an error
+ // during serving the special intercepted index.html can result in an empty
+ // response but a 200 status.
+ require.Contains(t, resp.Body.String(), `
+
+