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(), ` + +