Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Option to set "stale" consistency by default for http requests. #3142

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 41 additions & 13 deletions agent/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,17 @@ type DNSConfig struct {
RecursorTimeoutRaw string `mapstructure:"recursor_timeout" json:"-"`
}

// HTTPConfig is used to fine tune the Http sub-system.
type HTTPConfig struct {
// AllowStale is used to turn on stale consistency mode by default for HTTP API requests.
// This gives horizontal read scalability since any Consul server can service the query instead of
// only the leader.
AllowStale bool `mapstructure:"allow_stale"`

// ResponseHeaders are used to add HTTP header response fields to the HTTP API responses.
ResponseHeaders map[string]string `mapstructure:"response_headers"`
}

// RetryJoinEC2 is used to configure discovery of instances via Amazon's EC2 api
type RetryJoinEC2 struct {
// The AWS region to look for instances in
Expand Down Expand Up @@ -364,6 +375,9 @@ type Config struct {
// Domain is the DNS domain for the records. Defaults to "consul."
Domain string `mapstructure:"domain"`

// HTTP configuration
HTTPConfig HTTPConfig `mapstructure:"http_config"`

// Encryption key to use for the Serf communication
EncryptKey string `mapstructure:"encrypt" json:"-"`

Expand Down Expand Up @@ -702,9 +716,6 @@ type Config struct {
// send with the update check. This is used to deduplicate messages.
DisableAnonymousSignature bool `mapstructure:"disable_anonymous_signature"`

// HTTPAPIResponseHeaders are used to add HTTP header response fields to the HTTP API responses.
HTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers"`

// AEInterval controls the anti-entropy interval. This is how often
// the agent attempts to reconcile its local state with the server's
// representation of our state. Defaults to every 60s.
Expand Down Expand Up @@ -755,11 +766,12 @@ type Config struct {

// deprecated fields
// keep them exported since otherwise the error messages don't show up
DeprecatedAtlasInfrastructure string `mapstructure:"atlas_infrastructure" json:"-"`
DeprecatedAtlasToken string `mapstructure:"atlas_token" json:"-"`
DeprecatedAtlasACLToken string `mapstructure:"atlas_acl_token" json:"-"`
DeprecatedAtlasJoin bool `mapstructure:"atlas_join" json:"-"`
DeprecatedAtlasEndpoint string `mapstructure:"atlas_endpoint" json:"-"`
DeprecatedAtlasInfrastructure string `mapstructure:"atlas_infrastructure" json:"-"`
DeprecatedAtlasToken string `mapstructure:"atlas_token" json:"-"`
DeprecatedAtlasACLToken string `mapstructure:"atlas_acl_token" json:"-"`
DeprecatedAtlasJoin bool `mapstructure:"atlas_join" json:"-"`
DeprecatedAtlasEndpoint string `mapstructure:"atlas_endpoint" json:"-"`
DeprecatedHTTPAPIResponseHeaders map[string]string `mapstructure:"http_api_response_headers"`
}

// IncomingHTTPSConfig returns the TLS configuration for HTTPS
Expand Down Expand Up @@ -1363,6 +1375,19 @@ func DecodeConfig(r io.Reader) (*Config, error) {
result.TLSCipherSuites = ciphers
}

// This is for backwards compatibility.
// HTTPAPIResponseHeaders has been replaced with HttpApiConfig.ResponseHeaders
if len(result.DeprecatedHTTPAPIResponseHeaders) > 0 {
fmt.Fprintln(os.Stderr, "==> DEPRECATION: http_api_response_headers is deprecated and "+
"is no longer used. Please remove it from your configuration.")
if result.HTTPConfig.ResponseHeaders == nil {
result.HTTPConfig.ResponseHeaders = make(map[string]string)
}
for field, value := range result.DeprecatedHTTPAPIResponseHeaders {
result.HTTPConfig.ResponseHeaders[field] = value
}
result.DeprecatedHTTPAPIResponseHeaders = nil
}
return &result, nil
}

Expand Down Expand Up @@ -1966,14 +1991,17 @@ func MergeConfig(a, b *Config) *Config {
result.SessionTTLMin = b.SessionTTLMin
result.SessionTTLMinRaw = b.SessionTTLMinRaw
}
if len(b.HTTPAPIResponseHeaders) != 0 {
if result.HTTPAPIResponseHeaders == nil {
result.HTTPAPIResponseHeaders = make(map[string]string)
if len(b.HTTPConfig.ResponseHeaders) > 0 {
if result.HTTPConfig.ResponseHeaders == nil {
result.HTTPConfig.ResponseHeaders = make(map[string]string)
}
for field, value := range b.HTTPAPIResponseHeaders {
result.HTTPAPIResponseHeaders[field] = value
for field, value := range b.HTTPConfig.ResponseHeaders {
result.HTTPConfig.ResponseHeaders[field] = value
}
}
if b.HTTPConfig.AllowStale {
result.HTTPConfig.AllowStale = true
}
if len(b.Meta) != 0 {
if result.Meta == nil {
result.Meta = make(map[string]string)
Expand Down
12 changes: 9 additions & 3 deletions agent/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,11 @@ func TestDecodeConfig(t *testing.T) {
},
{
in: `{"http_api_response_headers":{"a":"b","c":"d"}}`,
c: &Config{HTTPAPIResponseHeaders: map[string]string{"a": "b", "c": "d"}},
c: &Config{HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{"a": "b", "c": "d"}}},
},
{
in: `{"http_config":{"response_headers":{"a":"b","c":"d"}}}`,
c: &Config{HTTPConfig: HTTPConfig{ResponseHeaders: map[string]string{"a": "b", "c": "d"}}},
},
{
in: `{"key_file":"a"}`,
Expand Down Expand Up @@ -1384,8 +1388,10 @@ func TestMergeConfig(t *testing.T) {
},
DisableUpdateCheck: true,
DisableAnonymousSignature: true,
HTTPAPIResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
HTTPConfig: HTTPConfig{
ResponseHeaders: map[string]string{
"Access-Control-Allow-Origin": "*",
},
},
UnixSockets: UnixSocketConfig{
UnixSocketPermissions{
Expand Down
15 changes: 9 additions & 6 deletions agent/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler {
// wrap is used to wrap functions to make them more convenient
func (s *HTTPServer) wrap(handler func(resp http.ResponseWriter, req *http.Request) (interface{}, error)) http.HandlerFunc {
return func(resp http.ResponseWriter, req *http.Request) {
setHeaders(resp, s.agent.config.HTTPAPIResponseHeaders)
setHeaders(resp, s.agent.config.HTTPConfig.ResponseHeaders)
setTranslateAddr(resp, s.agent.config.TranslateWanAddrs)

// Obfuscate any tokens from appearing in the logs
Expand Down Expand Up @@ -358,15 +358,17 @@ func parseWait(resp http.ResponseWriter, req *http.Request, b *structs.QueryOpti
}

// parseConsistency is used to parse the ?stale and ?consistent query params.
// allowStale forces stale consistency instead of default one if none was provided in the query.
// Returns true on error
func parseConsistency(resp http.ResponseWriter, req *http.Request, b *structs.QueryOptions) bool {
func parseConsistency(resp http.ResponseWriter, req *http.Request, allowStale bool, b *structs.QueryOptions) bool {
query := req.URL.Query()
if _, ok := query["stale"]; ok {
b.AllowStale = true
}
if _, ok := query["consistent"]; ok {
b.RequireConsistent = true
}
b.AllowStale = !b.RequireConsistent && allowStale
if _, ok := query["stale"]; ok {
b.AllowStale = true
}
if b.AllowStale && b.RequireConsistent {
resp.WriteHeader(http.StatusBadRequest) // 400
fmt.Fprint(resp, "Cannot specify ?stale with ?consistent, conflicting semantics.")
Expand Down Expand Up @@ -433,7 +435,8 @@ func (s *HTTPServer) parseMetaFilter(req *http.Request) map[string]string {
func (s *HTTPServer) parse(resp http.ResponseWriter, req *http.Request, dc *string, b *structs.QueryOptions) bool {
s.parseDC(req, dc)
s.parseToken(req, &b.Token)
if parseConsistency(resp, req, b) {
allowStale := s.agent.config.HTTPConfig.AllowStale
if parseConsistency(resp, req, allowStale, b) {
return true
}
return parseWait(resp, req, b)
Expand Down
53 changes: 29 additions & 24 deletions agent/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ func TestHTTPAPI_TranslateAddrHeader(t *testing.T) {
func TestHTTPAPIResponseHeaders(t *testing.T) {
t.Parallel()
cfg := TestConfig()
cfg.HTTPAPIResponseHeaders = map[string]string{
cfg.HTTPConfig.ResponseHeaders = map[string]string{
"Access-Control-Allow-Origin": "*",
"X-XSS-Protection": "1; mode=block",
}
Expand Down Expand Up @@ -436,31 +436,36 @@ func TestParseWait_InvalidIndex(t *testing.T) {
func TestParseConsistency(t *testing.T) {
t.Parallel()
resp := httptest.NewRecorder()
var b structs.QueryOptions

req, _ := http.NewRequest("GET", "/v1/catalog/nodes?stale", nil)
if d := parseConsistency(resp, req, &b); d {
t.Fatalf("unexpected done")
}

if !b.AllowStale {
t.Fatalf("Bad: %v", b)
}
if b.RequireConsistent {
t.Fatalf("Bad: %v", b)
}

b = structs.QueryOptions{}
req, _ = http.NewRequest("GET", "/v1/catalog/nodes?consistent", nil)
if d := parseConsistency(resp, req, &b); d {
t.Fatalf("unexpected done")
tests := []struct {
url string
allowStale bool
wantAllowStale bool
wantRequireConsistent bool
}{
{"/v1/catalog/nodes?stale", false, true, false},
{"/v1/catalog/nodes?stale", true, true, false},
{"/v1/catalog/nodes?consistent", false, false, true},
{"/v1/catalog/nodes?consistent", true, false, true},
{"/v1/catalog/nodes", false, false, false},
{"/v1/catalog/nodes", true, true, false},
}

if b.AllowStale {
t.Fatalf("Bad: %v", b)
}
if !b.RequireConsistent {
t.Fatalf("Bad: %v", b)
for _, tt := range tests {
name := fmt.Sprintf("url=%v, HTTP.AllowStale=%v", tt.url, tt.allowStale)
t.Run(name, func(t *testing.T) {
var q structs.QueryOptions
req, _ := http.NewRequest("GET", tt.url, nil)
if d := parseConsistency(resp, req, tt.allowStale, &q); d {
t.Fatalf("Failed to parse consistency.")
}
if got, want := q.AllowStale, tt.wantAllowStale; got != want {
t.Fatalf("got allowStale %v want %v", got, want)
}
if got, want := q.RequireConsistent, tt.wantRequireConsistent; got != want {
t.Fatalf("got requireConsistent %v want %v", got, want)
}
})
}
}

Expand All @@ -470,7 +475,7 @@ func TestParseConsistency_Invalid(t *testing.T) {
var b structs.QueryOptions

req, _ := http.NewRequest("GET", "/v1/catalog/nodes?stale&consistent", nil)
if d := parseConsistency(resp, req, &b); !d {
if d := parseConsistency(resp, req, false, &b); !d {
t.Fatalf("expected done")
}

Expand Down
38 changes: 24 additions & 14 deletions website/source/docs/agent/options.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,30 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
be increasingly uncommon to need to change this value with modern
resolvers).

* <a name="http_config"></a><a href="#http_config">`http_config`</a> This object allows a number
of sub-keys to be set which can tune how HTTP queries are serviced.
<br><br>
The following sub-keys are available:

* <a name="allow_stale"></a><a href="#allow_stale">`allow_stale`</a> - Enables a stale query
for HTTP queries. This allows any Consul server, rather than only the leader, to service
the request. The advantage of this is you get linear read scalability with Consul servers.
This defaults to false.

* <a name="http_api_response_headers"></a><a href="#http_api_response_headers">`http_api_response_headers`</a>
This object allows adding headers to the HTTP API
responses. For example, the following config can be used to enable
[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) on
the HTTP API endpoints:

```javascript
{
"http_api_response_headers": {
"Access-Control-Allow-Origin": "*"
}
}
```

* <a name="domain"></a><a href="#domain">`domain`</a> Equivalent to the
[`-domain` command-line flag](#_domain).

Expand Down Expand Up @@ -724,20 +748,6 @@ Consul will not enable TLS for the HTTP API unless the `https` port has been ass
PEM-encoded private key. The key is used with the certificate to verify the agent's authenticity.
This must be provided along with [`cert_file`](#cert_file).

* <a name="http_api_response_headers"></a><a href="#http_api_response_headers">`http_api_response_headers`</a>
This object allows adding headers to the HTTP API
responses. For example, the following config can be used to enable
[CORS](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing) on
the HTTP API endpoints:

```javascript
{
"http_api_response_headers": {
"Access-Control-Allow-Origin": "*"
}
}
```

* <a name="leave_on_terminate"></a><a href="#leave_on_terminate">`leave_on_terminate`</a> If
enabled, when the agent receives a TERM signal, it will send a `Leave` message to the rest
of the cluster and gracefully leave. The default behavior for this feature varies based on
Expand Down