From 526bab616463eebfd095bf6e389eee73c1bb4b4d Mon Sep 17 00:00:00 2001 From: Paul Banks Date: Thu, 10 Sep 2020 17:25:56 +0100 Subject: [PATCH 1/6] Add config changes for UI metrics --- agent/agent.go | 14 ++ agent/agent_test.go | 30 ++++ agent/config/builder.go | 134 ++++++++++++++--- agent/config/config.go | 261 ++++++++++++++++++--------------- agent/config/default.go | 4 +- agent/config/flags.go | 6 +- agent/config/runtime.go | 48 ++++-- agent/config/runtime_test.go | 274 +++++++++++++++++++++++++++++++---- agent/http.go | 35 ++++- agent/ui_endpoint_test.go | 2 +- 10 files changed, 612 insertions(+), 196 deletions(-) diff --git a/agent/agent.go b/agent/agent.go index 66827a8821..6d59ed0f5f 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -334,6 +334,9 @@ func New(bd BaseDeps) (*Agent, error) { cache: bd.Cache, } + // Initialize the UI Config + a.uiConfig.Store(a.config.UIConfig) + a.serviceManager = NewServiceManager(&a) // TODO: do this somewhere else, maybe move to newBaseDeps @@ -3823,3 +3826,14 @@ func defaultIfEmpty(val, defaultVal string) string { } return defaultVal } + +// getUIConfig is the canonical way to read the value of the UIConfig at +// runtime. It is thread safe and returns the most recent configuration which +// may have changed since the agent started due to config reload. +func (a *Agent) getUIConfig() config.UIConfig { + if cfg, ok := a.uiConfig.Load().(config.UIConfig); ok { + return cfg + } + // Shouldn't happen but be defensive + return config.UIConfig{} +} diff --git a/agent/agent_test.go b/agent/agent_test.go index 283e90c147..df3f0e75b2 100644 --- a/agent/agent_test.go +++ b/agent/agent_test.go @@ -3503,6 +3503,36 @@ func TestAgent_ReloadConfigTLSConfigFailure(t *testing.T) { require.Len(t, tlsConf.RootCAs.Subjects(), 1) } +func TestAgent_ReloadConfigUIConfig(t *testing.T) { + t.Parallel() + dataDir := testutil.TempDir(t, "agent") // we manage the data dir + hcl := ` + data_dir = "` + dataDir + `" + ui_config { + enabled = true // note that this is _not_ reloadable + metrics_provider = "foo" + } + ` + a := NewTestAgent(t, hcl) + defer a.Shutdown() + + uiCfg := a.getUIConfig() + require.Equal(t, "foo", uiCfg.MetricsProvider) + + hcl = ` + data_dir = "` + dataDir + `" + ui_config { + enabled = true + metrics_provider = "bar" + } + ` + c := TestConfig(testutil.Logger(t), config.FileSource{Name: t.Name(), Format: "hcl", Data: hcl}) + require.NoError(t, a.reloadConfigInternal(c)) + + uiCfg = a.getUIConfig() + require.Equal(t, "bar", uiCfg.MetricsProvider) +} + func TestAgent_consulConfig_AutoEncryptAllowTLS(t *testing.T) { t.Parallel() dataDir := testutil.TempDir(t, "agent") // we manage the data dir diff --git a/agent/config/builder.go b/agent/config/builder.go index 4b9aab1b7d..96a6035360 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), @@ -1094,7 +1112,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) { // 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_-]+$") + var reBasicName = regexp.MustCompile("^[a-z0-9_-]+$") + var reDatacenter = reBasicName // validContentPath defines a regexp for a valid content path name. var validContentPath = regexp.MustCompile(`^[A-Za-z0-9/_-]+$`) @@ -1113,12 +1132,50 @@ func (b *Builder) Validate(rt RuntimeConfig) error { 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 rt.UIConfig.MetricsProvider != "" && + !reBasicName.MatchString(rt.UIConfig.MetricsProvider) { + return fmt.Errorf("ui_config.metrics_provider can only contain lowercase "+ + "alphanumeric or _ characters. received: %q", rt.UIConfig.MetricsProvider) + } + 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 !reBasicName.MatchString(k) { + return fmt.Errorf("ui_config.dashboard_url_templates key names can only "+ + "contain lowercase alphanumeric or _ characters. received: %q", k) + } + 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 { @@ -1194,11 +1251,11 @@ func (b *Builder) Validate(rt RuntimeConfig) error { return fmt.Errorf("acl_datacenter cannot be %q. Please use only [a-z0-9-_]", rt.ACLDatacenter) } // 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 +1704,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 347bc09c28..df6bc979c6 100644 --- a/agent/config/default.go +++ b/agent/config/default.go @@ -139,7 +139,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 9b2da792ff..b92654c26b 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 }, }, @@ -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..cb649d1969 100644 --- a/agent/http.go +++ b/agent/http.go @@ -347,16 +347,23 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler { handlePProf("/debug/pprof/trace", pprof.Trace) if s.IsUIEnabled() { + // Note that we _don't_ support reloading ui_config.{enabled,content_dir} + // since this only runs at initial startup. + var uifs http.FileSystem // Use the custom UI dir if provided. - if s.agent.config.UIDir != "" { - uifs = http.Dir(s.agent.config.UIDir) + uiConfig := s.agent.getUIConfig() + if uiConfig.Dir != "" { + uifs = http.Dir(uiConfig.Dir) } else { fs := assetFS() uifs = fs } - uifs = &redirectFS{fs: &settingsInjectedIndexFS{fs: uifs, UISettings: s.GetUIENVFromConfig()}} + 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( @@ -368,9 +375,9 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler { uifsWithHeaders, ) mux.Handle( - s.agent.config.UIContentPath, + uiConfig.ContentPath, http.StripPrefix( - s.agent.config.UIContentPath, + uiConfig.ContentPath, uifsWithHeaders, ), ) @@ -391,9 +398,20 @@ func (s *HTTPHandlers) handler(enableDebug bool) http.Handler { } func (s *HTTPHandlers) GetUIENVFromConfig() map[string]interface{} { + uiCfg := s.agent.getUIConfig() + vars := map[string]interface{}{ - "CONSUL_CONTENT_PATH": s.agent.config.UIContentPath, - "CONSUL_ACLS_ENABLED": s.agent.config.ACLsEnabled, + "CONSUL_CONTENT_PATH": uiCfg.ContentPath, + "CONSUL_ACLS_ENABLED": s.agent.config.ACLsEnabled, + "CONSUL_METRICS_PROVIDER": uiCfg.MetricsProvider, + "CONSUL_METRICS_PROVIDER_OPTIONS": json.RawMessage(uiCfg.MetricsProviderOptionsJSON), + // We explicitly MUST NOT pass the metrics_proxy object since it might + // contain add_headers with secrets that the UI shouldn't know e.g. API + // tokens for the backend. The provider should either require the proxy to + // be configured and then use that or hit the backend directly from the + // browser. + "CONSUL_METRICS_PROXY_ENABLED": uiCfg.MetricsProxy.BaseURL != "", + "CONSUL_DASHBOARD_URL_TEMPLATES": uiCfg.DashboardURLTemplates, } s.addEnterpriseUIENVVars(vars) @@ -655,7 +673,8 @@ 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 + uiCfg := s.agent.getUIConfig() + http.Redirect(resp, req, uiCfg.ContentPath, http.StatusMovedPermanently) // 301 } func decodeBody(body io.Reader, out interface{}) error { diff --git a/agent/ui_endpoint_test.go b/agent/ui_endpoint_test.go index 5291398cdb..7338417e41 100644 --- a/agent/ui_endpoint_test.go +++ b/agent/ui_endpoint_test.go @@ -34,7 +34,7 @@ func TestUiIndex(t *testing.T) { testrpc.WaitForLeader(t, a.RPC, "dc1") // Create file - path := filepath.Join(a.Config.UIDir, "my-file") + path := filepath.Join(a.Config.UIConfig.Dir, "my-file") if err := ioutil.WriteFile(path, []byte("test"), 0777); err != nil { t.Fatalf("err: %v", err) } From 54a33efa4b2e5682282e536e13f92386f887432f Mon Sep 17 00:00:00 2001 From: Paul Banks Date: Wed, 16 Sep 2020 13:30:42 +0100 Subject: [PATCH 2/6] Fix JSON encoding of metrics options which broke the index but didn't break tests. Also add tests that do catch that error. --- agent/http.go | 16 ++++++++++++---- agent/http_test.go | 38 ++++++++++++++++++++++++++++++++++---- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/agent/http.go b/agent/http.go index cb649d1969..f7e0bf05b2 100644 --- a/agent/http.go +++ b/agent/http.go @@ -29,6 +29,7 @@ import ( "github.com/hashicorp/go-cleanhttp" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" + "github.com/ryboe/q" ) // MethodNotAllowedError should be returned by a handler when the HTTP method is not allowed. @@ -169,6 +170,7 @@ func (fs *settingsInjectedIndexFS) Open(name string) (http.File, error) { } content, err := ioutil.ReadAll(file) + q.Q(err) if err != nil { return nil, fmt.Errorf("failed reading index.html: %s", err) } @@ -182,6 +184,7 @@ func (fs *settingsInjectedIndexFS) Open(name string) (http.File, error) { // First built an escaped, JSON blob from the settings passed. bs, err := json.Marshal(fs.UISettings) + q.Q(string(bs)) if err != nil { return nil, fmt.Errorf("failed marshalling UI settings JSON: %s", err) } @@ -401,10 +404,9 @@ func (s *HTTPHandlers) GetUIENVFromConfig() map[string]interface{} { uiCfg := s.agent.getUIConfig() vars := map[string]interface{}{ - "CONSUL_CONTENT_PATH": uiCfg.ContentPath, - "CONSUL_ACLS_ENABLED": s.agent.config.ACLsEnabled, - "CONSUL_METRICS_PROVIDER": uiCfg.MetricsProvider, - "CONSUL_METRICS_PROVIDER_OPTIONS": json.RawMessage(uiCfg.MetricsProviderOptionsJSON), + "CONSUL_CONTENT_PATH": uiCfg.ContentPath, + "CONSUL_ACLS_ENABLED": s.agent.config.ACLsEnabled, + "CONSUL_METRICS_PROVIDER": uiCfg.MetricsProvider, // We explicitly MUST NOT pass the metrics_proxy object since it might // contain add_headers with secrets that the UI shouldn't know e.g. API // tokens for the backend. The provider should either require the proxy to @@ -414,6 +416,12 @@ func (s *HTTPHandlers) GetUIENVFromConfig() map[string]interface{} { "CONSUL_DASHBOARD_URL_TEMPLATES": uiCfg.DashboardURLTemplates, } + // Only set this if there is some actual JSON or we'll cause a JSON + // marshalling error later during serving which ends up being silent. + if uiCfg.MetricsProviderOptionsJSON != "" { + vars["CONSUL_METRICS_PROVIDER_OPTIONS"] = json.RawMessage(uiCfg.MetricsProviderOptionsJSON) + } + s.addEnterpriseUIENVVars(vars) return vars diff --git a/agent/http_test.go b/agent/http_test.go index 233ae0e180..d927b3ac80 100644 --- a/agent/http_test.go +++ b/agent/http_test.go @@ -1198,16 +1198,46 @@ 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 in + // settingsInjectedIndexFS.Open will actually result in http.FileServer just + // serving a plain directory listing instead which still passes the above HTTP + // status assertion. This comment is part of our index.html template + require.Contains(t, resp.Body.String(), ` + + From 3ff5901be8170bfe5746950f9d4bb3fabe2925e5 Mon Sep 17 00:00:00 2001 From: Paul Banks Date: Thu, 1 Oct 2020 12:26:19 +0100 Subject: [PATCH 5/6] Fix ui dir where there is no index tests and lint issue. --- agent/ui_endpoint_test.go | 8 ++++--- agent/uiserver/ui_template_data.go | 3 +++ agent/uiserver/uiserver.go | 36 +++++++++++++++++++++--------- agent/uiserver/uiserver_test.go | 23 +++++++++++++++++++ 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/agent/ui_endpoint_test.go b/agent/ui_endpoint_test.go index 7338417e41..006aa841dd 100644 --- a/agent/ui_endpoint_test.go +++ b/agent/ui_endpoint_test.go @@ -28,18 +28,20 @@ func TestUiIndex(t *testing.T) { // Make the server a := NewTestAgent(t, ` - ui_dir = "`+uiDir+`" + ui_config { + dir = "`+uiDir+`" + } `) defer a.Shutdown() testrpc.WaitForLeader(t, a.RPC, "dc1") // Create file path := filepath.Join(a.Config.UIConfig.Dir, "my-file") - if err := ioutil.WriteFile(path, []byte("test"), 0777); err != nil { + if err := ioutil.WriteFile(path, []byte("test"), 0644); err != nil { t.Fatalf("err: %v", err) } - // Register node + // Request the custom file req, _ := http.NewRequest("GET", "/ui/my-file", nil) req.URL.Scheme = "http" req.URL.Host = a.HTTPAddr() diff --git a/agent/uiserver/ui_template_data.go b/agent/uiserver/ui_template_data.go index cb80f42354..13852ba683 100644 --- a/agent/uiserver/ui_template_data.go +++ b/agent/uiserver/ui_template_data.go @@ -35,6 +35,9 @@ func uiTemplateDataFromConfig(cfg *config.RuntimeConfig) (map[string]interface{} } err := uiTemplateDataFromConfigEnterprise(cfg, d, uiCfg) + if err != nil { + return nil, err + } // Render uiCfg down to JSON ready to inject into the template bs, err := json.Marshal(uiCfg) diff --git a/agent/uiserver/uiserver.go b/agent/uiserver/uiserver.go index 2c3d807fef..436f847a8e 100644 --- a/agent/uiserver/uiserver.go +++ b/agent/uiserver/uiserver.go @@ -88,22 +88,36 @@ func (h *Handler) ReloadConfig(newCfg *config.RuntimeConfig) error { // Render a new index.html with the new config values ready to serve. buf, info, err := renderIndex(newCfg, fs) - if err != nil { + if _, ok := err.(*os.PathError); ok && newCfg.UIConfig.Dir != "" { + // A Path error indicates that there is no index.html. This could happen if + // the user configured their own UI dir and is serving something that is not + // our usual UI. This won't work perfectly because our uiserver will still + // redirect everything to the UI but we shouldn't fail the entire UI server + // with a 500 in this case. Partly that's just bad UX and partly it's a + // breaking change although quite an edge case. Instead, continue but just + // return a 404 response for the index.html and log a warning. + h.logger.Warn("ui_config.dir does not contain an index.html. Index templating and redirects to index.html are disabled.") + } else if err != nil { return err } - // Create a new fs that serves the rendered index file or falls back to the - // underlying FS. - fs = &bufIndexFS{ - fs: fs, - indexRendered: buf, - indexInfo: info, + // buf can be nil in the PathError case above. We should skip this part but + // still serve the rest of the files in that case. + if buf != nil { + // Create a new fs that serves the rendered index file or falls back to the + // underlying FS. + fs = &bufIndexFS{ + fs: fs, + indexRendered: buf, + indexInfo: info, + } + + // Wrap the buffering FS our redirect FS. This needs to happen later so that + // redirected requests for /index.html get served the rendered version not the + // original. + fs = &redirectFS{fs: fs} } - // Wrap the buffering FS our redirect FS. This needs to happen later so that - // redirected requests for /index.html get served the rendered version not the - // original. - fs = &redirectFS{fs: fs} newState.srv = http.FileServer(fs) // Store the new state diff --git a/agent/uiserver/uiserver_test.go b/agent/uiserver/uiserver_test.go index 6ab43eb731..ea6f623b34 100644 --- a/agent/uiserver/uiserver_test.go +++ b/agent/uiserver/uiserver_test.go @@ -2,9 +2,12 @@ package uiserver import ( "encoding/json" + "io/ioutil" "net/http" "net/http/httptest" "net/url" + "os" + "path/filepath" "regexp" "testing" @@ -228,3 +231,23 @@ func TestReload(t *testing.T) { require.Contains(t, rec.Body.String(), "exotic-metrics-provider-name") } } + +func TestCustomDir(t *testing.T) { + uiDir := testutil.TempDir(t, "consul-uiserver") + defer os.RemoveAll(uiDir) + + path := filepath.Join(uiDir, "test-file") + require.NoError(t, ioutil.WriteFile(path, []byte("test"), 0644)) + + cfg := basicUIEnabledConfig() + cfg.UIConfig.Dir = uiDir + h := NewHandler(cfg, testutil.Logger(t)) + + req := httptest.NewRequest("GET", "/test-file", nil) + rec := httptest.NewRecorder() + + h.ServeHTTP(rec, req) + + require.Equal(t, http.StatusOK, rec.Code) + require.Contains(t, rec.Body.String(), "test") +} From 3b8125a24d57f04468268196b0e3402318a2ca0a Mon Sep 17 00:00:00 2001 From: Paul Banks Date: Thu, 1 Oct 2020 16:19:10 +0100 Subject: [PATCH 6/6] Update all the references in CI and makefile to the bindata file location --- .circleci/config.yml | 10 +++++----- GNUmakefile | 2 +- agent/uiserver/buffered_file.go | 2 +- build-support/functions/30-release.sh | 2 +- build-support/functions/40-publish.sh | 2 +- codecov.yml | 2 +- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ad43dd92d0..ab618b7583 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 87758e688f..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) diff --git a/agent/uiserver/buffered_file.go b/agent/uiserver/buffered_file.go index 040ae3aae1..13aa84e030 100644 --- a/agent/uiserver/buffered_file.go +++ b/agent/uiserver/buffered_file.go @@ -7,7 +7,7 @@ import ( "time" ) -// bufferedFile implements os.File and allows us to modify a file from disk by +// bufferedFile implements http.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. type bufferedFile struct { diff --git a/build-support/functions/30-release.sh b/build-support/functions/30-release.sh index c55e8f1fec..37135f48e8 100644 --- a/build-support/functions/30-release.sh +++ b/build-support/functions/30-release.sh @@ -477,7 +477,7 @@ function build_release { if is_set "${do_tag}" then - git add "${sdir}/agent/bindata_assetfs.go" + git add "${sdir}/agent/uiserver/bindata_assetfs.go" if test $? -ne 0 then err "ERROR: Failed to git add the assetfs file" diff --git a/build-support/functions/40-publish.sh b/build-support/functions/40-publish.sh index 8e511e32f5..c1ed2be0a2 100644 --- a/build-support/functions/40-publish.sh +++ b/build-support/functions/40-publish.sh @@ -99,7 +99,7 @@ function confirm_git_push_changes { ;; ?) # bindata_assetfs.go will make these meaningless - git_diff "$(pwd)" ":!agent/bindata_assetfs.go"|| ret 1 + git_diff "$(pwd)" ":!agent/uiserver/bindata_assetfs.go"|| ret 1 answer="" ;; * ) diff --git a/codecov.yml b/codecov.yml index 33606c5bab..9e6e403021 100644 --- a/codecov.yml +++ b/codecov.yml @@ -39,6 +39,6 @@ github_checks: annotations: false ignore: - - "agent/bindata_assetfs.go" + - "agent/uiserver/bindata_assetfs.go" - "vendor/**/*" - "**/*.pb.go"