Resolve conflicts against master

This commit is contained in:
freddygv 2020-09-11 18:41:58 -06:00
commit 33af8dab9a
460 changed files with 10828 additions and 5645 deletions

3
.changelog/8537.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
api: Fixed a panic caused by an api request with Connect=null
```

3
.changelog/8552.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
cache: Config parameters for cache throttling are now reloaded automatically on agent reload. Restarting the agent is not needed anymore.
```

3
.changelog/8588.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
connect: fix renewing secondary intermediate certificates
```

3
.changelog/8596.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
connect: all config entries pick up a meta field
```

3
.changelog/8601.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
connect: fix bug in preventing some namespaced config entry modifications
```

3
.changelog/8602.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
api: Allow for the client to use TLS over a Unix domain socket.
```

3
.changelog/8603.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
telemetry: track node and service counts and emit them as metrics
```

3
.changelog/8606.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:bug
connect: `connect envoy` command now respects the `-ca-path` flag
```

3
.changelog/_8621.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
snapshot agent: Deregister critical snapshotting TTL check if leadership is transferred.
```

View File

@ -19,7 +19,7 @@ references:
EMAIL: noreply@hashicorp.com EMAIL: noreply@hashicorp.com
GIT_AUTHOR_NAME: circleci-consul GIT_AUTHOR_NAME: circleci-consul
GIT_COMMITTER_NAME: circleci-consul GIT_COMMITTER_NAME: circleci-consul
S3_ARTIFACT_BUCKET: consul-dev-artifacts S3_ARTIFACT_BUCKET: consul-dev-artifacts-v2
BASH_ENV: .circleci/bash_env.sh BASH_ENV: .circleci/bash_env.sh
VAULT_BINARY_VERSION: 1.2.2 VAULT_BINARY_VERSION: 1.2.2
@ -33,6 +33,27 @@ steps:
curl -sSL "${url}/v${GOTESTSUM_RELEASE}/gotestsum_${GOTESTSUM_RELEASE}_linux_amd64.tar.gz" | \ curl -sSL "${url}/v${GOTESTSUM_RELEASE}/gotestsum_${GOTESTSUM_RELEASE}_linux_amd64.tar.gz" | \
sudo tar -xz --overwrite -C /usr/local/bin gotestsum sudo tar -xz --overwrite -C /usr/local/bin gotestsum
get-aws-cli: &get-aws-cli
run:
name: download and install AWS CLI
command: |
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
echo -e "${AWS_CLI_GPG_KEY}" | gpg --import
curl -o awscliv2.sig https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip.sig
gpg --verify awscliv2.sig awscliv2.zip
unzip awscliv2.zip
sudo ./aws/install
aws-assume-role: &aws-assume-role
run:
name: assume-role aws creds
command: |
# assume role has duration of 15 min (the minimum allowed)
CREDENTIALS="$(aws sts assume-role --duration-seconds 900 --role-arn ${ROLE_ARN} --role-session-name build-${CIRCLE_SHA1} | jq '.Credentials')"
echo "export AWS_ACCESS_KEY_ID=$(echo $CREDENTIALS | jq -r '.AccessKeyId')" >> $BASH_ENV
echo "export AWS_SECRET_ACCESS_KEY=$(echo $CREDENTIALS | jq -r '.SecretAccessKey')" >> $BASH_ENV
echo "export AWS_SESSION_TOKEN=$(echo $CREDENTIALS | jq -r '.SessionToken')" >> $BASH_ENV
# This step MUST be at the end of any set of steps due to the 'when' condition # This step MUST be at the end of any set of steps due to the 'when' condition
notify-slack-failure: &notify-slack-failure notify-slack-failure: &notify-slack-failure
name: notify-slack-failure name: notify-slack-failure
@ -389,13 +410,13 @@ jobs:
# upload development build to s3 # upload development build to s3
dev-upload-s3: dev-upload-s3:
docker: docker:
- image: circleci/python:stretch - image: *GOLANG_IMAGE
environment: environment:
<<: *ENVIRONMENT <<: *ENVIRONMENT
steps: steps:
- run: - checkout
name: Install awscli - *get-aws-cli
command: sudo pip install awscli - *aws-assume-role
# get consul binary # get consul binary
- attach_workspace: - attach_workspace:
at: bin/ at: bin/

View File

@ -1,5 +1,33 @@
## UNRELEASED ## UNRELEASED
## 1.8.4 (September 11, 2020)
FEATURES:
* agent: expose the list of supported envoy versions on /v1/agent/self [[GH-8545](https://github.com/hashicorp/consul/issues/8545)]
* cache: Config parameters for cache throttling are now reloaded automatically on agent reload. Restarting the agent is not needed anymore. [[GH-8552](https://github.com/hashicorp/consul/issues/8552)]
* connect: all config entries pick up a meta field [[GH-8596](https://github.com/hashicorp/consul/issues/8596)]
IMPROVEMENTS:
* api: Added `ACLMode` method to the `AgentMember` type to determine what ACL mode the agent is operating in. [[GH-8575](https://github.com/hashicorp/consul/issues/8575)]
* api: Added `IsConsulServer` method to the `AgentMember` type to easily determine whether the agent is a server. [[GH-8575](https://github.com/hashicorp/consul/issues/8575)]
* api: Added constants for common tag keys and values in the `Tags` field of the `AgentMember` struct. [[GH-8575](https://github.com/hashicorp/consul/issues/8575)]
* api: Allow for the client to use TLS over a Unix domain socket. [[GH-8602](https://github.com/hashicorp/consul/issues/8602)]
* api: `GET v1/operator/keyring` also lists primary keys. [[GH-8522](https://github.com/hashicorp/consul/issues/8522)]
* connect: Add support for http2 and grpc to ingress gateways [[GH-8458](https://github.com/hashicorp/consul/issues/8458)]
* serf: update to `v0.9.4` which supports primary keys in the ListKeys operation. [[GH-8522](https://github.com/hashicorp/consul/issues/8522)]
BUGFIXES:
* connect: use stronger validation that ingress gateways have compatible protocols defined for their upstreams [[GH-8494](https://github.com/hashicorp/consul/issues/8494)]
* agent: ensure that we normalize bootstrapped config entries [[GH-8547](https://github.com/hashicorp/consul/issues/8547)]
* api: Fixed a panic caused by an api request with Connect=null [[GH-8537](https://github.com/hashicorp/consul/issues/8537)]
* connect: `connect envoy` command now respects the `-ca-path` flag [[GH-8606](https://github.com/hashicorp/consul/issues/8606)]
* connect: fix bug in preventing some namespaced config entry modifications [[GH-8601](https://github.com/hashicorp/consul/issues/8601)]
* connect: fix renewing secondary intermediate certificates [[GH-8588](https://github.com/hashicorp/consul/issues/8588)]
* ui: fixed a bug related to in-folder KV creation [GH-8613](https://github.com/hashicorp/consul/pull/8613)
## 1.8.3 (August 12, 2020) ## 1.8.3 (August 12, 2020)
BUGFIXES: BUGFIXES:
@ -116,6 +144,17 @@ BUGFIXES:
* ui: Miscellaneous amends for Safari and Firefox [[GH-7904](https://github.com/hashicorp/consul/issues/7904)] [[GH-7907](https://github.com/hashicorp/consul/pull/7907)] * ui: Miscellaneous amends for Safari and Firefox [[GH-7904](https://github.com/hashicorp/consul/issues/7904)] [[GH-7907](https://github.com/hashicorp/consul/pull/7907)]
* ui: Ensure a value is always passed to CONSUL_SSO_ENABLED [[GH-7913](https://github.com/hashicorp/consul/pull/7913)] * ui: Ensure a value is always passed to CONSUL_SSO_ENABLED [[GH-7913](https://github.com/hashicorp/consul/pull/7913)]
## 1.7.8 (September 11, 2020)
FEATURES:
* agent: expose the list of supported envoy versions on /v1/agent/self [[GH-8545](https://github.com/hashicorp/consul/issues/8545)]
BUG FIXES:
* connect: fix bug in preventing some namespaced config entry modifications [[GH-8601](https://github.com/hashicorp/consul/issues/8601)]
* api: fixed a panic caused by an api request with Connect=null [[GH-8537](https://github.com/hashicorp/consul/pull/8537)]
## 1.7.7 (August 12, 2020) ## 1.7.7 (August 12, 2020)
BUGFIXES: BUGFIXES:
@ -127,7 +166,7 @@ BUGFIXES:
BUG FIXES: BUG FIXES:
* [backport/1.7.x] xds: revert setting set_node_on_first_message_only to true when generating envoy bootstrap config [[GH-8441](https://github.com/hashicorp/consul/issues/8441)] * xds: revert setting set_node_on_first_message_only to true when generating envoy bootstrap config [[GH-8441](https://github.com/hashicorp/consul/issues/8441)]
## 1.7.5 (July 30, 2020) ## 1.7.5 (July 30, 2020)
@ -340,6 +379,12 @@ BUGFIXES:
* ui: Discovery-Chain: Improve parsing of redirects [[GH-7174](https://github.com/hashicorp/consul/pull/7174)] * ui: Discovery-Chain: Improve parsing of redirects [[GH-7174](https://github.com/hashicorp/consul/pull/7174)]
* ui: Fix styling of duplicate intention error message [[GH6936]](https://github.com/hashicorp/consul/pull/6936) * ui: Fix styling of duplicate intention error message [[GH6936]](https://github.com/hashicorp/consul/pull/6936)
## 1.6.9 (September 11, 2020)
BUG FIXES:
* api: fixed a panic caused by an api request with Connect=null [[GH-8537](https://github.com/hashicorp/consul/pull/8537)]
## 1.6.8 (August 12, 2020) ## 1.6.8 (August 12, 2020)
BUG FIXES: BUG FIXES:

View File

@ -1,7 +1,7 @@
# Consul [![CircleCI](https://circleci.com/gh/hashicorp/consul/tree/master.svg?style=svg)](https://circleci.com/gh/hashicorp/consul/tree/master) [![Discuss](https://img.shields.io/badge/discuss-consul-ca2171.svg?style=flat)](https://discuss.hashicorp.com/c/consul) # Consul [![CircleCI](https://circleci.com/gh/hashicorp/consul/tree/master.svg?style=svg)](https://circleci.com/gh/hashicorp/consul/tree/master) [![Discuss](https://img.shields.io/badge/discuss-consul-ca2171.svg?style=flat)](https://discuss.hashicorp.com/c/consul)
* Website: https://www.consul.io * Website: https://www.consul.io
* Tutorials: [https://learn.hashicorp.com](https://learn.hashicorp.com/consul) * Tutorials: [HashiCorp Learn](https://learn.hashicorp.com/consul)
* Forum: [Discuss](https://discuss.hashicorp.com/c/consul) * Forum: [Discuss](https://discuss.hashicorp.com/c/consul)
Consul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure. Consul is a distributed, highly available, and data center aware solution to connect and configure applications across dynamic, distributed infrastructure.
@ -10,12 +10,12 @@ Consul provides several key features:
* **Multi-Datacenter** - Consul is built to be datacenter aware, and can * **Multi-Datacenter** - Consul is built to be datacenter aware, and can
support any number of regions without complex configuration. support any number of regions without complex configuration.
* **Service Mesh/Service Segmentation** - Consul Connect enables secure service-to-service * **Service Mesh/Service Segmentation** - Consul Connect enables secure service-to-service
communication with automatic TLS encryption and identity-based authorization. Applications communication with automatic TLS encryption and identity-based authorization. Applications
can use sidecar proxies in a service mesh configuration to establish TLS can use sidecar proxies in a service mesh configuration to establish TLS
connections for inbound and outbound connections without being aware of Connect at all. connections for inbound and outbound connections without being aware of Connect at all.
* **Service Discovery** - Consul makes it simple for services to register * **Service Discovery** - Consul makes it simple for services to register
themselves and to discover other services via a DNS or HTTP interface. themselves and to discover other services via a DNS or HTTP interface.
External services such as SaaS providers can be registered as well. External services such as SaaS providers can be registered as well.
@ -41,9 +41,10 @@ contacting us at security@hashicorp.com.
A few quick start guides are available on the Consul website: A few quick start guides are available on the Consul website:
* **Standalone binary install:** https://learn.hashicorp.com/consul/getting-started/install * **Standalone binary install:** https://learn.hashicorp.com/tutorials/consul/get-started-install
* **Minikube install:** https://learn.hashicorp.com/consul/kubernetes/minikube * **Minikube install:** https://learn.hashicorp.com/tutorials/consul/kubernetes-minikube
* **Kubernetes install:** https://learn.hashicorp.com/consul/kubernetes/kubernetes-deployment-guide * **Kind install:** https://learn.hashicorp.com/tutorials/consul/kubernetes-kind
* **Kubernetes install:** https://learn.hashicorp.com/tutorials/consul/kubernetes-deployment-guide
## Documentation ## Documentation

View File

@ -184,7 +184,9 @@ func TestACL_AgentMasterToken(t *testing.T) {
t.Parallel() t.Parallel()
a := NewTestACLAgent(t, t.Name(), TestACLConfig(), nil, nil) a := NewTestACLAgent(t, t.Name(), TestACLConfig(), nil, nil)
a.loadTokens(a.config) err := a.tokens.Load(a.config.ACLTokens, a.logger)
require.NoError(t, err)
authz, err := a.resolveToken("towel") authz, err := a.resolveToken("towel")
require.NotNil(t, authz) require.NotNil(t, authz)
require.Nil(t, err) require.Nil(t, err)

View File

@ -19,6 +19,7 @@ import (
"github.com/hashicorp/consul/agent/dns" "github.com/hashicorp/consul/agent/dns"
"github.com/hashicorp/consul/agent/router" "github.com/hashicorp/consul/agent/router"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/go-connlimit" "github.com/hashicorp/go-connlimit"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-memdb" "github.com/hashicorp/go-memdb"
@ -31,7 +32,6 @@ import (
autoconf "github.com/hashicorp/consul/agent/auto-config" autoconf "github.com/hashicorp/consul/agent/auto-config"
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types" cachetype "github.com/hashicorp/consul/agent/cache-types"
certmon "github.com/hashicorp/consul/agent/cert-monitor"
"github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/checks"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/consul" "github.com/hashicorp/consul/agent/consul"
@ -40,7 +40,6 @@ import (
"github.com/hashicorp/consul/agent/proxycfg" "github.com/hashicorp/consul/agent/proxycfg"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/systemd" "github.com/hashicorp/consul/agent/systemd"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/agent/xds" "github.com/hashicorp/consul/agent/xds"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/api/watch" "github.com/hashicorp/consul/api/watch"
@ -68,9 +67,6 @@ const (
checksDir = "checks" checksDir = "checks"
checkStateDir = "checks/state" checkStateDir = "checks/state"
// Name of the file tokens will be persisted within
tokensPath = "acl-tokens.json"
// Default reasons for node/service maintenance mode // Default reasons for node/service maintenance mode
defaultNodeMaintReason = "Maintenance mode is enabled for this node, " + defaultNodeMaintReason = "Maintenance mode is enabled for this node, " +
"but no reason was provided. This is a default message." "but no reason was provided. This is a default message."
@ -162,8 +158,6 @@ type notifier interface {
type Agent struct { type Agent struct {
autoConf *autoconf.AutoConfig autoConf *autoconf.AutoConfig
certMonitor *certmon.CertMonitor
// config is the agent configuration. // config is the agent configuration.
config *config.RuntimeConfig config *config.RuntimeConfig
@ -262,10 +256,12 @@ type Agent struct {
// dnsServer provides the DNS API // dnsServer provides the DNS API
dnsServers []*DNSServer dnsServers []*DNSServer
// httpServers provides the HTTP API on various endpoints // apiServers listening for connections. If any of these server goroutines
httpServers []*HTTPServer // fail, the agent will be shutdown.
apiServers *apiServers
// wgServers is the wait group for all HTTP and DNS servers // wgServers is the wait group for all HTTP and DNS servers
// TODO: remove once dnsServers are handled by apiServers
wgServers sync.WaitGroup wgServers sync.WaitGroup
// watchPlans tracks all the currently-running watch plans for the // watchPlans tracks all the currently-running watch plans for the
@ -295,11 +291,6 @@ type Agent struct {
// based on the current consul configuration. // based on the current consul configuration.
tlsConfigurator *tlsutil.Configurator tlsConfigurator *tlsutil.Configurator
// persistedTokensLock is used to synchronize access to the persisted token
// store within the data directory. This will prevent loading while writing as
// well as multiple concurrent writes.
persistedTokensLock sync.RWMutex
// httpConnLimiter is used to limit connections to the HTTP server by client // httpConnLimiter is used to limit connections to the HTTP server by client
// IP. // IP.
httpConnLimiter connlimit.Limiter httpConnLimiter connlimit.Limiter
@ -373,6 +364,12 @@ func New(bd BaseDeps) (*Agent, error) {
// pass the agent itself so its safe to move here. // pass the agent itself so its safe to move here.
a.registerCache() a.registerCache()
// TODO: why do we ignore failure to load persisted tokens?
_ = a.tokens.Load(bd.RuntimeConfig.ACLTokens, a.logger)
// TODO: pass in a fully populated apiServers into Agent.New
a.apiServers = NewAPIServers(a.logger)
return &a, nil return &a, nil
} }
@ -426,11 +423,6 @@ func (a *Agent) Start(ctx context.Context) error {
return fmt.Errorf("Failed to load TLS configurations after applying auto-config settings: %w", err) return fmt.Errorf("Failed to load TLS configurations after applying auto-config settings: %w", err)
} }
// TODO: move to newBaseDeps
// TODO: handle error
a.loadTokens(a.config)
a.loadEnterpriseTokens(a.config)
// create the local state // create the local state
a.State = local.NewState(LocalConfig(c), a.logger, a.tokens) a.State = local.NewState(LocalConfig(c), a.logger, a.tokens)
@ -495,43 +487,6 @@ func (a *Agent) Start(ctx context.Context) error {
a.State.Delegate = a.delegate a.State.Delegate = a.delegate
a.State.TriggerSyncChanges = a.sync.SyncChanges.Trigger a.State.TriggerSyncChanges = a.sync.SyncChanges.Trigger
if a.config.AutoEncryptTLS && !a.config.ServerMode {
reply, err := a.autoEncryptInitialCertificate(ctx)
if err != nil {
return fmt.Errorf("AutoEncrypt failed: %s", err)
}
cmConfig := new(certmon.Config).
WithCache(a.cache).
WithLogger(a.logger.Named(logging.AutoEncrypt)).
WithTLSConfigurator(a.tlsConfigurator).
WithTokens(a.tokens).
WithFallback(a.autoEncryptInitialCertificate).
WithDNSSANs(a.config.AutoEncryptDNSSAN).
WithIPSANs(a.config.AutoEncryptIPSAN).
WithDatacenter(a.config.Datacenter).
WithNodeName(a.config.NodeName)
monitor, err := certmon.New(cmConfig)
if err != nil {
return fmt.Errorf("AutoEncrypt failed to setup certificate monitor: %w", err)
}
if err := monitor.Update(reply); err != nil {
return fmt.Errorf("AutoEncrypt failed to setup certificate monitor: %w", err)
}
a.certMonitor = monitor
// we don't need to worry about ever calling Stop as we have tied the go routines
// to the agents lifetime by using the StopCh. Also the agent itself doesn't have
// a need of ensuring that the go routine was stopped before performing any action
// so we can ignore the chan in the return.
if _, err := a.certMonitor.Start(&lib.StopChannelContext{StopCh: a.shutdownCh}); err != nil {
return fmt.Errorf("AutoEncrypt failed to start certificate monitor: %w", err)
}
a.logger.Info("automatically upgraded to TLS")
}
if err := a.autoConf.Start(&lib.StopChannelContext{StopCh: a.shutdownCh}); err != nil { if err := a.autoConf.Start(&lib.StopChannelContext{StopCh: a.shutdownCh}); err != nil {
return fmt.Errorf("AutoConf failed to start certificate monitor: %w", err) return fmt.Errorf("AutoConf failed to start certificate monitor: %w", err)
} }
@ -620,10 +575,7 @@ func (a *Agent) Start(ctx context.Context) error {
// Start HTTP and HTTPS servers. // Start HTTP and HTTPS servers.
for _, srv := range servers { for _, srv := range servers {
if err := a.serveHTTP(srv); err != nil { a.apiServers.Start(srv)
return err
}
a.httpServers = append(a.httpServers, srv)
} }
// Start gRPC server. // Start gRPC server.
@ -645,17 +597,10 @@ func (a *Agent) Start(ctx context.Context) error {
return nil return nil
} }
func (a *Agent) autoEncryptInitialCertificate(ctx context.Context) (*structs.SignedResponse, error) { // Failed returns a channel which is closed when the first server goroutine exits
client := a.delegate.(*consul.Client) // with a non-nil error.
func (a *Agent) Failed() <-chan struct{} {
addrs := a.config.StartJoinAddrsLAN return a.apiServers.failed
disco, err := newDiscover()
if err != nil && len(addrs) == 0 {
return nil, err
}
addrs = append(addrs, retryJoinAddrs(disco, retryJoinSerfVariant, "LAN", a.config.RetryJoinLAN, a.logger)...)
return client.RequestAutoEncryptCerts(ctx, addrs, a.config.ServerPort, a.tokens.AgentToken(), a.config.AutoEncryptDNSSAN, a.config.AutoEncryptIPSAN)
} }
func (a *Agent) listenAndServeGRPC() error { func (a *Agent) listenAndServeGRPC() error {
@ -790,14 +735,16 @@ func (a *Agent) startListeners(addrs []net.Addr) ([]net.Listener, error) {
// //
// This approach should ultimately be refactored to the point where we just // This approach should ultimately be refactored to the point where we just
// start the server and any error should trigger a proper shutdown of the agent. // start the server and any error should trigger a proper shutdown of the agent.
func (a *Agent) listenHTTP() ([]*HTTPServer, error) { func (a *Agent) listenHTTP() ([]apiServer, error) {
var ln []net.Listener var ln []net.Listener
var servers []*HTTPServer var servers []apiServer
start := func(proto string, addrs []net.Addr) error { start := func(proto string, addrs []net.Addr) error {
listeners, err := a.startListeners(addrs) listeners, err := a.startListeners(addrs)
if err != nil { if err != nil {
return err return err
} }
ln = append(ln, listeners...)
for _, l := range listeners { for _, l := range listeners {
var tlscfg *tls.Config var tlscfg *tls.Config
@ -807,18 +754,15 @@ func (a *Agent) listenHTTP() ([]*HTTPServer, error) {
l = tls.NewListener(l, tlscfg) l = tls.NewListener(l, tlscfg)
} }
srv := &HTTPServer{
agent: a,
denylist: NewDenylist(a.config.HTTPBlockEndpoints),
}
httpServer := &http.Server{ httpServer := &http.Server{
Addr: l.Addr().String(), Addr: l.Addr().String(),
TLSConfig: tlscfg, TLSConfig: tlscfg,
Handler: srv.handler(a.config.EnableDebug),
} }
srv := &HTTPServer{
Server: httpServer,
ln: l,
agent: a,
denylist: NewDenylist(a.config.HTTPBlockEndpoints),
proto: proto,
}
httpServer.Handler = srv.handler(a.config.EnableDebug)
// Load the connlimit helper into the server // Load the connlimit helper into the server
connLimitFn := a.httpConnLimiter.HTTPConnStateFuncWithDefault429Handler(10 * time.Millisecond) connLimitFn := a.httpConnLimiter.HTTPConnStateFuncWithDefault429Handler(10 * time.Millisecond)
@ -831,27 +775,39 @@ func (a *Agent) listenHTTP() ([]*HTTPServer, error) {
httpServer.ConnState = connLimitFn httpServer.ConnState = connLimitFn
} }
ln = append(ln, l) servers = append(servers, apiServer{
servers = append(servers, srv) Protocol: proto,
Addr: l.Addr(),
Shutdown: httpServer.Shutdown,
Run: func() error {
err := httpServer.Serve(l)
if err == nil || err == http.ErrServerClosed {
return nil
}
return fmt.Errorf("%s server %s failed: %w", proto, l.Addr(), err)
},
})
} }
return nil return nil
} }
if err := start("http", a.config.HTTPAddrs); err != nil { if err := start("http", a.config.HTTPAddrs); err != nil {
for _, l := range ln { closeListeners(ln)
l.Close()
}
return nil, err return nil, err
} }
if err := start("https", a.config.HTTPSAddrs); err != nil { if err := start("https", a.config.HTTPSAddrs); err != nil {
for _, l := range ln { closeListeners(ln)
l.Close()
}
return nil, err return nil, err
} }
return servers, nil return servers, nil
} }
func closeListeners(lns []net.Listener) {
for _, l := range lns {
l.Close()
}
}
// setupHTTPS adds HTTP/2 support, ConnState, and a connection handshake timeout // setupHTTPS adds HTTP/2 support, ConnState, and a connection handshake timeout
// to the http.Server. // to the http.Server.
func setupHTTPS(server *http.Server, connState func(net.Conn, http.ConnState), timeout time.Duration) error { func setupHTTPS(server *http.Server, connState func(net.Conn, http.ConnState), timeout time.Duration) error {
@ -913,43 +869,6 @@ func (a *Agent) listenSocket(path string) (net.Listener, error) {
return l, nil return l, nil
} }
func (a *Agent) serveHTTP(srv *HTTPServer) error {
// https://github.com/golang/go/issues/20239
//
// In go.8.1 there is a race between Serve and Shutdown. If
// Shutdown is called before the Serve go routine was scheduled then
// the Serve go routine never returns. This deadlocks the agent
// shutdown for some tests since it will wait forever.
notif := make(chan net.Addr)
a.wgServers.Add(1)
go func() {
defer a.wgServers.Done()
notif <- srv.ln.Addr()
err := srv.Server.Serve(srv.ln)
if err != nil && err != http.ErrServerClosed {
a.logger.Error("error closing server", "error", err)
}
}()
select {
case addr := <-notif:
if srv.proto == "https" {
a.logger.Info("Started HTTPS server",
"address", addr.String(),
"network", addr.Network(),
)
} else {
a.logger.Info("Started HTTP server",
"address", addr.String(),
"network", addr.Network(),
)
}
return nil
case <-time.After(time.Second):
return fmt.Errorf("agent: timeout starting HTTP servers")
}
}
// stopAllWatches stops all the currently running watches // stopAllWatches stops all the currently running watches
func (a *Agent) stopAllWatches() { func (a *Agent) stopAllWatches() {
for _, wp := range a.watchPlans { for _, wp := range a.watchPlans {
@ -1380,12 +1299,6 @@ func (a *Agent) ShutdownAgent() error {
// this should help them to be stopped more quickly // this should help them to be stopped more quickly
a.autoConf.Stop() a.autoConf.Stop()
if a.certMonitor != nil {
// this would be cancelled anyways (by the closing of the shutdown ch)
// but this should help them to be stopped more quickly
a.certMonitor.Stop()
}
// Stop the service manager (must happen before we take the stateLock to avoid deadlock) // Stop the service manager (must happen before we take the stateLock to avoid deadlock)
if a.serviceManager != nil { if a.serviceManager != nil {
a.serviceManager.Stop() a.serviceManager.Stop()
@ -1454,13 +1367,12 @@ func (a *Agent) ShutdownAgent() error {
// ShutdownEndpoints terminates the HTTP and DNS servers. Should be // ShutdownEndpoints terminates the HTTP and DNS servers. Should be
// preceded by ShutdownAgent. // preceded by ShutdownAgent.
// TODO: remove this method, move to ShutdownAgent
func (a *Agent) ShutdownEndpoints() { func (a *Agent) ShutdownEndpoints() {
a.shutdownLock.Lock() a.shutdownLock.Lock()
defer a.shutdownLock.Unlock() defer a.shutdownLock.Unlock()
if len(a.dnsServers) == 0 && len(a.httpServers) == 0 { ctx := context.TODO()
return
}
for _, srv := range a.dnsServers { for _, srv := range a.dnsServers {
if srv.Server != nil { if srv.Server != nil {
@ -1474,27 +1386,11 @@ func (a *Agent) ShutdownEndpoints() {
} }
a.dnsServers = nil a.dnsServers = nil
for _, srv := range a.httpServers { a.apiServers.Shutdown(ctx)
a.logger.Info("Stopping server",
"protocol", strings.ToUpper(srv.proto),
"address", srv.ln.Addr().String(),
"network", srv.ln.Addr().Network(),
)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
srv.Server.Shutdown(ctx)
if ctx.Err() == context.DeadlineExceeded {
a.logger.Warn("Timeout stopping server",
"protocol", strings.ToUpper(srv.proto),
"address", srv.ln.Addr().String(),
"network", srv.ln.Addr().Network(),
)
}
}
a.httpServers = nil
a.logger.Info("Waiting for endpoints to shut down") a.logger.Info("Waiting for endpoints to shut down")
a.wgServers.Wait() if err := a.apiServers.WaitForShutdown(); err != nil {
a.logger.Error(err.Error())
}
a.logger.Info("Endpoints down") a.logger.Info("Endpoints down")
} }
@ -3446,90 +3342,6 @@ func (a *Agent) unloadChecks() error {
return nil return nil
} }
type persistedTokens struct {
Replication string `json:"replication,omitempty"`
AgentMaster string `json:"agent_master,omitempty"`
Default string `json:"default,omitempty"`
Agent string `json:"agent,omitempty"`
}
func (a *Agent) getPersistedTokens() (*persistedTokens, error) {
persistedTokens := &persistedTokens{}
if !a.config.ACLEnableTokenPersistence {
return persistedTokens, nil
}
a.persistedTokensLock.RLock()
defer a.persistedTokensLock.RUnlock()
tokensFullPath := filepath.Join(a.config.DataDir, tokensPath)
buf, err := ioutil.ReadFile(tokensFullPath)
if err != nil {
if os.IsNotExist(err) {
// non-existence is not an error we care about
return persistedTokens, nil
}
return persistedTokens, fmt.Errorf("failed reading tokens file %q: %s", tokensFullPath, err)
}
if err := json.Unmarshal(buf, persistedTokens); err != nil {
return persistedTokens, fmt.Errorf("failed to decode tokens file %q: %s", tokensFullPath, err)
}
return persistedTokens, nil
}
func (a *Agent) loadTokens(conf *config.RuntimeConfig) error {
persistedTokens, persistenceErr := a.getPersistedTokens()
if persistenceErr != nil {
a.logger.Warn("unable to load persisted tokens", "error", persistenceErr)
}
if persistedTokens.Default != "" {
a.tokens.UpdateUserToken(persistedTokens.Default, token.TokenSourceAPI)
if conf.ACLToken != "" {
a.logger.Warn("\"default\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
a.tokens.UpdateUserToken(conf.ACLToken, token.TokenSourceConfig)
}
if persistedTokens.Agent != "" {
a.tokens.UpdateAgentToken(persistedTokens.Agent, token.TokenSourceAPI)
if conf.ACLAgentToken != "" {
a.logger.Warn("\"agent\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
a.tokens.UpdateAgentToken(conf.ACLAgentToken, token.TokenSourceConfig)
}
if persistedTokens.AgentMaster != "" {
a.tokens.UpdateAgentMasterToken(persistedTokens.AgentMaster, token.TokenSourceAPI)
if conf.ACLAgentMasterToken != "" {
a.logger.Warn("\"agent_master\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
a.tokens.UpdateAgentMasterToken(conf.ACLAgentMasterToken, token.TokenSourceConfig)
}
if persistedTokens.Replication != "" {
a.tokens.UpdateReplicationToken(persistedTokens.Replication, token.TokenSourceAPI)
if conf.ACLReplicationToken != "" {
a.logger.Warn("\"replication\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
a.tokens.UpdateReplicationToken(conf.ACLReplicationToken, token.TokenSourceConfig)
}
return persistenceErr
}
// snapshotCheckState is used to snapshot the current state of the health // snapshotCheckState is used to snapshot the current state of the health
// checks. This is done before we reload our checks, so that we can properly // checks. This is done before we reload our checks, so that we can properly
// restore into the same state. // restore into the same state.
@ -3709,8 +3521,7 @@ func (a *Agent) reloadConfigInternal(newCfg *config.RuntimeConfig) error {
// Reload tokens - should be done before all the other loading // Reload tokens - should be done before all the other loading
// to ensure the correct tokens are available for attaching to // to ensure the correct tokens are available for attaching to
// the checks and service registrations. // the checks and service registrations.
a.loadTokens(newCfg) a.tokens.Load(newCfg.ACLTokens, a.logger)
a.loadEnterpriseTokens(newCfg)
if err := a.tlsConfigurator.Update(newCfg.ToTLSUtilConfig()); err != nil { if err := a.tlsConfigurator.Update(newCfg.ToTLSUtilConfig()); err != nil {
return fmt.Errorf("Failed reloading tls configuration: %s", err) return fmt.Errorf("Failed reloading tls configuration: %s", err)
@ -3764,6 +3575,12 @@ func (a *Agent) reloadConfigInternal(newCfg *config.RuntimeConfig) error {
return err return err
} }
if a.cache.ReloadOptions(newCfg.Cache) {
a.logger.Info("Cache options have been updated")
} else {
a.logger.Debug("Cache options have not been modified")
}
// Update filtered metrics // Update filtered metrics
metrics.UpdateFilter(newCfg.Telemetry.AllowedPrefixes, metrics.UpdateFilter(newCfg.Telemetry.AllowedPrefixes,
newCfg.Telemetry.BlockedPrefixes) newCfg.Telemetry.BlockedPrefixes)

View File

@ -1,10 +1,8 @@
package agent package agent
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
@ -21,7 +19,6 @@ import (
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/file"
"github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/logging/monitor" "github.com/hashicorp/consul/logging/monitor"
"github.com/hashicorp/consul/types" "github.com/hashicorp/consul/types"
@ -1233,79 +1230,42 @@ func (s *HTTPServer) AgentToken(resp http.ResponseWriter, req *http.Request) (in
return nil, nil return nil, nil
} }
if s.agent.config.ACLEnableTokenPersistence {
// we hold the lock around updating the internal token store
// as well as persisting the tokens because we don't want to write
// into the store to have something else wipe it out before we can
// persist everything (like an agent config reload). The token store
// lock is only held for those operations so other go routines that
// just need to read some token out of the store will not be impacted
// any more than they would be without token persistence.
s.agent.persistedTokensLock.Lock()
defer s.agent.persistedTokensLock.Unlock()
}
// Figure out the target token. // Figure out the target token.
target := strings.TrimPrefix(req.URL.Path, "/v1/agent/token/") target := strings.TrimPrefix(req.URL.Path, "/v1/agent/token/")
triggerAntiEntropySync := false
switch target { err = s.agent.tokens.WithPersistenceLock(func() error {
case "acl_token", "default": triggerAntiEntropySync := false
changed := s.agent.tokens.UpdateUserToken(args.Token, token_store.TokenSourceAPI) switch target {
if changed { case "acl_token", "default":
triggerAntiEntropySync = true changed := s.agent.tokens.UpdateUserToken(args.Token, token_store.TokenSourceAPI)
if changed {
triggerAntiEntropySync = true
}
case "acl_agent_token", "agent":
changed := s.agent.tokens.UpdateAgentToken(args.Token, token_store.TokenSourceAPI)
if changed {
triggerAntiEntropySync = true
}
case "acl_agent_master_token", "agent_master":
s.agent.tokens.UpdateAgentMasterToken(args.Token, token_store.TokenSourceAPI)
case "acl_replication_token", "replication":
s.agent.tokens.UpdateReplicationToken(args.Token, token_store.TokenSourceAPI)
default:
return NotFoundError{Reason: fmt.Sprintf("Token %q is unknown", target)}
} }
case "acl_agent_token", "agent": // TODO: is it safe to move this out of WithPersistenceLock?
changed := s.agent.tokens.UpdateAgentToken(args.Token, token_store.TokenSourceAPI) if triggerAntiEntropySync {
if changed { s.agent.sync.SyncFull.Trigger()
triggerAntiEntropySync = true
}
case "acl_agent_master_token", "agent_master":
s.agent.tokens.UpdateAgentMasterToken(args.Token, token_store.TokenSourceAPI)
case "acl_replication_token", "replication":
s.agent.tokens.UpdateReplicationToken(args.Token, token_store.TokenSourceAPI)
default:
resp.WriteHeader(http.StatusNotFound)
fmt.Fprintf(resp, "Token %q is unknown", target)
return nil, nil
}
if triggerAntiEntropySync {
s.agent.sync.SyncFull.Trigger()
}
if s.agent.config.ACLEnableTokenPersistence {
tokens := persistedTokens{}
if tok, source := s.agent.tokens.UserTokenAndSource(); tok != "" && source == token_store.TokenSourceAPI {
tokens.Default = tok
}
if tok, source := s.agent.tokens.AgentTokenAndSource(); tok != "" && source == token_store.TokenSourceAPI {
tokens.Agent = tok
}
if tok, source := s.agent.tokens.AgentMasterTokenAndSource(); tok != "" && source == token_store.TokenSourceAPI {
tokens.AgentMaster = tok
}
if tok, source := s.agent.tokens.ReplicationTokenAndSource(); tok != "" && source == token_store.TokenSourceAPI {
tokens.Replication = tok
}
data, err := json.Marshal(tokens)
if err != nil {
s.agent.logger.Warn("failed to persist tokens", "error", err)
return nil, fmt.Errorf("Failed to marshal tokens for persistence: %v", err)
}
if err := file.WriteAtomicWithPerms(filepath.Join(s.agent.config.DataDir, tokensPath), data, 0700, 0600); err != nil {
s.agent.logger.Warn("failed to persist tokens", "error", err)
return nil, fmt.Errorf("Failed to persist tokens - %v", err)
} }
return nil
})
if err != nil {
return nil, err
} }
s.agent.logger.Info("Updated agent's ACL token", "token", target) s.agent.logger.Info("Updated agent's ACL token", "token", target)

View File

@ -4774,13 +4774,14 @@ func TestAgent_Token(t *testing.T) {
init tokens init tokens
raw tokens raw tokens
effective tokens effective tokens
expectedErr error
}{ }{
{ {
name: "bad token name", name: "bad token name",
method: "PUT", method: "PUT",
url: "nope?token=root", url: "nope?token=root",
body: body("X"), body: body("X"),
code: http.StatusNotFound, expectedErr: NotFoundError{Reason: `Token "nope" is unknown`},
}, },
{ {
name: "bad JSON", name: "bad JSON",
@ -4942,7 +4943,12 @@ func TestAgent_Token(t *testing.T) {
url := fmt.Sprintf("/v1/agent/token/%s", tt.url) url := fmt.Sprintf("/v1/agent/token/%s", tt.url)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
req, _ := http.NewRequest(tt.method, url, tt.body) req, _ := http.NewRequest(tt.method, url, tt.body)
_, err := a.srv.AgentToken(resp, req) _, err := a.srv.AgentToken(resp, req)
if tt.expectedErr != nil {
require.Equal(t, tt.expectedErr, err)
return
}
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tt.code, resp.Code) require.Equal(t, tt.code, resp.Code)
require.Equal(t, tt.effective.user, a.tokens.UserToken()) require.Equal(t, tt.effective.user, a.tokens.UserToken())

View File

@ -23,10 +23,6 @@ func (a *Agent) initEnterprise(consulCfg *consul.Config) error {
return nil return nil
} }
// loadEnterpriseTokens is a noop stub for the func defined agent_ent.go
func (a *Agent) loadEnterpriseTokens(conf *config.RuntimeConfig) {
}
// reloadEnterprise is a noop stub for the func defined agent_ent.go // reloadEnterprise is a noop stub for the func defined agent_ent.go
func (a *Agent) reloadEnterprise(conf *config.RuntimeConfig) error { func (a *Agent) reloadEnterprise(conf *config.RuntimeConfig) error {
return nil return nil

View File

@ -43,6 +43,7 @@ import (
"github.com/hashicorp/serf/serf" "github.com/hashicorp/serf/serf"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/time/rate"
"gopkg.in/square/go-jose.v2/jwt" "gopkg.in/square/go-jose.v2/jwt"
) )
@ -765,10 +766,18 @@ func TestCacheRateLimit(test *testing.T) {
test.Run(fmt.Sprintf("rate_limit_at_%v", currentTest.rateLimit), func(t *testing.T) { test.Run(fmt.Sprintf("rate_limit_at_%v", currentTest.rateLimit), func(t *testing.T) {
tt := currentTest tt := currentTest
t.Parallel() t.Parallel()
a := NewTestAgent(t, fmt.Sprintf("cache = { entry_fetch_rate = %v, entry_fetch_max_burst = 1 }", tt.rateLimit)) a := NewTestAgent(t, "cache = { entry_fetch_rate = 1, entry_fetch_max_burst = 100 }")
defer a.Shutdown() defer a.Shutdown()
testrpc.WaitForTestAgent(t, a.RPC, "dc1") testrpc.WaitForTestAgent(t, a.RPC, "dc1")
cfg := a.config
require.Equal(t, rate.Limit(1), a.config.Cache.EntryFetchRate)
require.Equal(t, 100, a.config.Cache.EntryFetchMaxBurst)
cfg.Cache.EntryFetchRate = rate.Limit(tt.rateLimit)
cfg.Cache.EntryFetchMaxBurst = 1
a.reloadConfigInternal(cfg)
require.Equal(t, rate.Limit(tt.rateLimit), a.config.Cache.EntryFetchRate)
require.Equal(t, 1, a.config.Cache.EntryFetchMaxBurst)
var wg sync.WaitGroup var wg sync.WaitGroup
stillProcessing := true stillProcessing := true
@ -1908,7 +1917,7 @@ func TestAgent_HTTPCheck_EnableAgentTLSForChecks(t *testing.T) {
Status: api.HealthCritical, Status: api.HealthCritical,
} }
url := fmt.Sprintf("https://%s/v1/agent/self", a.srv.ln.Addr().String()) url := fmt.Sprintf("https://%s/v1/agent/self", a.HTTPAddr())
chk := &structs.CheckType{ chk := &structs.CheckType{
HTTP: url, HTTP: url,
Interval: 20 * time.Millisecond, Interval: 20 * time.Millisecond,
@ -3336,163 +3345,6 @@ func TestAgent_reloadWatchesHTTPS(t *testing.T) {
} }
} }
func TestAgent_loadTokens(t *testing.T) {
t.Parallel()
a := NewTestAgent(t, `
acl = {
enabled = true
tokens = {
agent = "alfa"
agent_master = "bravo",
default = "charlie"
replication = "delta"
}
}
`)
defer a.Shutdown()
require := require.New(t)
tokensFullPath := filepath.Join(a.config.DataDir, tokensPath)
t.Run("original-configuration", func(t *testing.T) {
require.Equal("alfa", a.tokens.AgentToken())
require.Equal("bravo", a.tokens.AgentMasterToken())
require.Equal("charlie", a.tokens.UserToken())
require.Equal("delta", a.tokens.ReplicationToken())
})
t.Run("updated-configuration", func(t *testing.T) {
cfg := &config.RuntimeConfig{
ACLToken: "echo",
ACLAgentToken: "foxtrot",
ACLAgentMasterToken: "golf",
ACLReplicationToken: "hotel",
}
// ensures no error for missing persisted tokens file
require.NoError(a.loadTokens(cfg))
require.Equal("echo", a.tokens.UserToken())
require.Equal("foxtrot", a.tokens.AgentToken())
require.Equal("golf", a.tokens.AgentMasterToken())
require.Equal("hotel", a.tokens.ReplicationToken())
})
t.Run("persisted-tokens", func(t *testing.T) {
cfg := &config.RuntimeConfig{
ACLToken: "echo",
ACLAgentToken: "foxtrot",
ACLAgentMasterToken: "golf",
ACLReplicationToken: "hotel",
}
tokens := `{
"agent" : "india",
"agent_master" : "juliett",
"default": "kilo",
"replication" : "lima"
}`
require.NoError(ioutil.WriteFile(tokensFullPath, []byte(tokens), 0600))
require.NoError(a.loadTokens(cfg))
// no updates since token persistence is not enabled
require.Equal("echo", a.tokens.UserToken())
require.Equal("foxtrot", a.tokens.AgentToken())
require.Equal("golf", a.tokens.AgentMasterToken())
require.Equal("hotel", a.tokens.ReplicationToken())
a.config.ACLEnableTokenPersistence = true
require.NoError(a.loadTokens(cfg))
require.Equal("india", a.tokens.AgentToken())
require.Equal("juliett", a.tokens.AgentMasterToken())
require.Equal("kilo", a.tokens.UserToken())
require.Equal("lima", a.tokens.ReplicationToken())
})
t.Run("persisted-tokens-override", func(t *testing.T) {
tokens := `{
"agent" : "mike",
"agent_master" : "november",
"default": "oscar",
"replication" : "papa"
}`
cfg := &config.RuntimeConfig{
ACLToken: "quebec",
ACLAgentToken: "romeo",
ACLAgentMasterToken: "sierra",
ACLReplicationToken: "tango",
}
require.NoError(ioutil.WriteFile(tokensFullPath, []byte(tokens), 0600))
require.NoError(a.loadTokens(cfg))
require.Equal("mike", a.tokens.AgentToken())
require.Equal("november", a.tokens.AgentMasterToken())
require.Equal("oscar", a.tokens.UserToken())
require.Equal("papa", a.tokens.ReplicationToken())
})
t.Run("partial-persisted", func(t *testing.T) {
tokens := `{
"agent" : "uniform",
"agent_master" : "victor"
}`
cfg := &config.RuntimeConfig{
ACLToken: "whiskey",
ACLAgentToken: "xray",
ACLAgentMasterToken: "yankee",
ACLReplicationToken: "zulu",
}
require.NoError(ioutil.WriteFile(tokensFullPath, []byte(tokens), 0600))
require.NoError(a.loadTokens(cfg))
require.Equal("uniform", a.tokens.AgentToken())
require.Equal("victor", a.tokens.AgentMasterToken())
require.Equal("whiskey", a.tokens.UserToken())
require.Equal("zulu", a.tokens.ReplicationToken())
})
t.Run("persistence-error-not-json", func(t *testing.T) {
cfg := &config.RuntimeConfig{
ACLToken: "one",
ACLAgentToken: "two",
ACLAgentMasterToken: "three",
ACLReplicationToken: "four",
}
require.NoError(ioutil.WriteFile(tokensFullPath, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0600))
err := a.loadTokens(cfg)
require.Error(err)
require.Equal("one", a.tokens.UserToken())
require.Equal("two", a.tokens.AgentToken())
require.Equal("three", a.tokens.AgentMasterToken())
require.Equal("four", a.tokens.ReplicationToken())
})
t.Run("persistence-error-wrong-top-level", func(t *testing.T) {
cfg := &config.RuntimeConfig{
ACLToken: "alfa",
ACLAgentToken: "bravo",
ACLAgentMasterToken: "charlie",
ACLReplicationToken: "foxtrot",
}
require.NoError(ioutil.WriteFile(tokensFullPath, []byte("[1,2,3]"), 0600))
err := a.loadTokens(cfg)
require.Error(err)
require.Equal("alfa", a.tokens.UserToken())
require.Equal("bravo", a.tokens.AgentToken())
require.Equal("charlie", a.tokens.AgentMasterToken())
require.Equal("foxtrot", a.tokens.ReplicationToken())
})
}
func TestAgent_SecurityChecks(t *testing.T) { func TestAgent_SecurityChecks(t *testing.T) {
t.Parallel() t.Parallel()
hcl := ` hcl := `

94
agent/apiserver.go Normal file
View File

@ -0,0 +1,94 @@
package agent
import (
"context"
"net"
"sync"
"time"
"github.com/hashicorp/go-hclog"
"golang.org/x/sync/errgroup"
)
// apiServers is a wrapper around errgroup.Group for managing go routines for
// long running agent components (ex: http server, dns server). If any of the
// servers fail, the failed channel will be closed, which will cause the agent
// to be shutdown instead of running in a degraded state.
//
// This struct exists as a shim for using errgroup.Group without making major
// changes to Agent. In the future it may be removed and replaced with more
// direct usage of errgroup.Group.
type apiServers struct {
logger hclog.Logger
group *errgroup.Group
servers []apiServer
// failed channel is closed when the first server goroutines exit with a
// non-nil error.
failed <-chan struct{}
}
type apiServer struct {
// Protocol supported by this server. One of: dns, http, https
Protocol string
// Addr the server is listening on
Addr net.Addr
// Run will be called in a goroutine to run the server. When any Run exits
// with a non-nil error, the failed channel will be closed.
Run func() error
// Shutdown function used to stop the server
Shutdown func(context.Context) error
}
// NewAPIServers returns an empty apiServers that is ready to Start servers.
func NewAPIServers(logger hclog.Logger) *apiServers {
group, ctx := errgroup.WithContext(context.TODO())
return &apiServers{
logger: logger,
group: group,
failed: ctx.Done(),
}
}
func (s *apiServers) Start(srv apiServer) {
srv.logger(s.logger).Info("Starting server")
s.servers = append(s.servers, srv)
s.group.Go(srv.Run)
}
func (s apiServer) logger(base hclog.Logger) hclog.Logger {
return base.With(
"protocol", s.Protocol,
"address", s.Addr.String(),
"network", s.Addr.Network())
}
// Shutdown all the servers and log any errors as warning. Each server is given
// 1 second, or until ctx is cancelled, to shutdown gracefully.
func (s *apiServers) Shutdown(ctx context.Context) {
shutdownGroup := new(sync.WaitGroup)
for i := range s.servers {
server := s.servers[i]
shutdownGroup.Add(1)
go func() {
defer shutdownGroup.Done()
logger := server.logger(s.logger)
logger.Info("Stopping server")
ctx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Warn("Failed to stop server")
}
}()
}
s.servers = nil
shutdownGroup.Wait()
}
// WaitForShutdown waits until all server goroutines have exited. Shutdown
// must be called before WaitForShutdown, otherwise it will block forever.
func (s *apiServers) WaitForShutdown() error {
return s.group.Wait()
}

65
agent/apiserver_test.go Normal file
View File

@ -0,0 +1,65 @@
package agent
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
)
func TestAPIServers_WithServiceRunError(t *testing.T) {
servers := NewAPIServers(hclog.New(nil))
server1, chErr1 := newAPIServerStub()
server2, _ := newAPIServerStub()
t.Run("Start", func(t *testing.T) {
servers.Start(server1)
servers.Start(server2)
select {
case <-servers.failed:
t.Fatalf("expected servers to still be running")
case <-time.After(5 * time.Millisecond):
}
})
err := fmt.Errorf("oops, I broke")
t.Run("server exit non-nil error", func(t *testing.T) {
chErr1 <- err
select {
case <-servers.failed:
case <-time.After(time.Second):
t.Fatalf("expected failed channel to be closed")
}
})
t.Run("shutdown remaining services", func(t *testing.T) {
servers.Shutdown(context.Background())
require.Equal(t, err, servers.WaitForShutdown())
})
}
func newAPIServerStub() (apiServer, chan error) {
chErr := make(chan error)
return apiServer{
Protocol: "http",
Addr: &net.TCPAddr{
IP: net.ParseIP("127.0.0.11"),
Port: 5505,
},
Run: func() error {
return <-chErr
},
Shutdown: func(ctx context.Context) error {
close(chErr)
return nil
},
}, chErr
}

View File

@ -4,62 +4,54 @@ import (
"context" "context"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net" "sync"
"os"
"path/filepath"
"strconv"
"strings"
"time" "time"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/connect" "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/proto/pbautoconf" "github.com/hashicorp/consul/proto/pbautoconf"
"github.com/hashicorp/go-discover"
discoverk8s "github.com/hashicorp/go-discover/provider/k8s"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
"github.com/golang/protobuf/jsonpb"
)
const (
// autoConfigFileName is the name of the file that the agent auto-config settings are
// stored in within the data directory
autoConfigFileName = "auto-config.json"
dummyTrustDomain = "dummytrustdomain"
)
var (
pbMarshaler = &jsonpb.Marshaler{
OrigName: false,
EnumsAsInts: false,
Indent: " ",
EmitDefaults: true,
}
pbUnmarshaler = &jsonpb.Unmarshaler{
AllowUnknownFields: false,
}
) )
// AutoConfig is all the state necessary for being able to parse a configuration // AutoConfig is all the state necessary for being able to parse a configuration
// as well as perform the necessary RPCs to perform Agent Auto Configuration. // as well as perform the necessary RPCs to perform Agent Auto Configuration.
//
// NOTE: This struct and methods on it are not currently thread/goroutine safe.
// However it doesn't spawn any of its own go routines yet and is used in a
// synchronous fashion. In the future if either of those two conditions change
// then we will need to add some locking here. I am deferring that for now
// to help ease the review of this already large PR.
type AutoConfig struct { type AutoConfig struct {
sync.Mutex
acConfig Config acConfig Config
logger hclog.Logger logger hclog.Logger
certMonitor CertMonitor cache Cache
waiter *lib.RetryWaiter
config *config.RuntimeConfig config *config.RuntimeConfig
autoConfigResponse *pbautoconf.AutoConfigResponse autoConfigResponse *pbautoconf.AutoConfigResponse
autoConfigSource config.Source autoConfigSource config.Source
running bool
done chan struct{}
// cancel is used to cancel the entire AutoConfig
// go routine. This is the main field protected
// by the mutex as it being non-nil indicates that
// the go routine has been started and is stoppable.
// note that it doesn't indcate that the go routine
// is currently running.
cancel context.CancelFunc
// cancelWatches is used to cancel the existing
// cache watches regarding the agents certificate. This is
// mainly only necessary when the Agent token changes.
cancelWatches context.CancelFunc
// cacheUpdates is the chan used to have the cache
// send us back events
cacheUpdates chan cache.UpdateEvent
// tokenUpdates is the struct used to receive
// events from the token store when the Agent
// token is updated.
tokenUpdates token.Notifier
} }
// New creates a new AutoConfig object for providing automatic Consul configuration. // New creates a new AutoConfig object for providing automatic Consul configuration.
@ -69,6 +61,19 @@ func New(config Config) (*AutoConfig, error) {
return nil, fmt.Errorf("must provide a config loader") return nil, fmt.Errorf("must provide a config loader")
case config.DirectRPC == nil: case config.DirectRPC == nil:
return nil, fmt.Errorf("must provide a direct RPC delegate") return nil, fmt.Errorf("must provide a direct RPC delegate")
case config.Cache == nil:
return nil, fmt.Errorf("must provide a cache")
case config.TLSConfigurator == nil:
return nil, fmt.Errorf("must provide a TLS configurator")
case config.Tokens == nil:
return nil, fmt.Errorf("must provide a token store")
}
if config.FallbackLeeway == 0 {
config.FallbackLeeway = 10 * time.Second
}
if config.FallbackRetry == 0 {
config.FallbackRetry = time.Minute
} }
logger := config.Logger logger := config.Logger
@ -83,15 +88,16 @@ func New(config Config) (*AutoConfig, error) {
} }
return &AutoConfig{ return &AutoConfig{
acConfig: config, acConfig: config,
logger: logger, logger: logger,
certMonitor: config.CertMonitor,
}, nil }, nil
} }
// ReadConfig will parse the current configuration and inject any // ReadConfig will parse the current configuration and inject any
// auto-config sources if present into the correct place in the parsing chain. // auto-config sources if present into the correct place in the parsing chain.
func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) { func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
ac.Lock()
defer ac.Unlock()
cfg, warnings, err := ac.acConfig.Loader(ac.autoConfigSource) cfg, warnings, err := ac.acConfig.Loader(ac.autoConfigSource)
if err != nil { if err != nil {
return cfg, err return cfg, err
@ -105,46 +111,6 @@ func (ac *AutoConfig) ReadConfig() (*config.RuntimeConfig, error) {
return cfg, nil return cfg, nil
} }
// restorePersistedAutoConfig will attempt to load the persisted auto-config
// settings from the data directory. It returns true either when there was an
// unrecoverable error or when the configuration was successfully loaded from
// disk. Recoverable errors, such as "file not found" are suppressed and this
// method will return false for the first boolean.
func (ac *AutoConfig) restorePersistedAutoConfig() (bool, error) {
if ac.config.DataDir == "" {
// no data directory means we don't have anything to potentially load
return false, nil
}
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
ac.logger.Debug("attempting to restore any persisted configuration", "path", path)
content, err := ioutil.ReadFile(path)
if err == nil {
rdr := strings.NewReader(string(content))
var resp pbautoconf.AutoConfigResponse
if err := pbUnmarshaler.Unmarshal(rdr, &resp); err != nil {
return false, fmt.Errorf("failed to decode persisted auto-config data: %w", err)
}
if err := ac.update(&resp); err != nil {
return false, fmt.Errorf("error restoring persisted auto-config response: %w", err)
}
ac.logger.Info("restored persisted configuration", "path", path)
return true, nil
}
if !os.IsNotExist(err) {
return true, fmt.Errorf("failed to load %s: %w", path, err)
}
// ignore non-existence errors as that is an indicator that we haven't
// performed the auto configuration before
return false, nil
}
// InitialConfiguration will perform a one-time RPC request to the configured servers // InitialConfiguration will perform a one-time RPC request to the configured servers
// to retrieve various cluster wide configurations. See the proto/pbautoconf/auto_config.proto // to retrieve various cluster wide configurations. See the proto/pbautoconf/auto_config.proto
// file for a complete reference of what configurations can be applied in this manner. // file for a complete reference of what configurations can be applied in this manner.
@ -164,30 +130,49 @@ func (ac *AutoConfig) InitialConfiguration(ctx context.Context) (*config.Runtime
ac.config = config ac.config = config
} }
if !ac.config.AutoConfig.Enabled { switch {
return ac.config, nil case ac.config.AutoConfig.Enabled:
} resp, err := ac.readPersistedAutoConfig()
if err != nil {
ready, err := ac.restorePersistedAutoConfig()
if err != nil {
return nil, err
}
if !ready {
ac.logger.Info("retrieving initial agent auto configuration remotely")
if err := ac.getInitialConfiguration(ctx); err != nil {
return nil, err return nil, err
} }
}
// re-read the configuration now that we have our initial auto-config if resp == nil {
config, err := ac.ReadConfig() ac.logger.Info("retrieving initial agent auto configuration remotely")
if err != nil { resp, err = ac.getInitialConfiguration(ctx)
return nil, err if err != nil {
} return nil, err
}
}
ac.config = config ac.logger.Debug("updating auto-config settings")
return ac.config, nil if err = ac.recordInitialConfiguration(resp); err != nil {
return nil, err
}
// re-read the configuration now that we have our initial auto-config
config, err := ac.ReadConfig()
if err != nil {
return nil, err
}
ac.config = config
return ac.config, nil
case ac.config.AutoEncryptTLS:
certs, err := ac.autoEncryptInitialCerts(ctx)
if err != nil {
return nil, err
}
if err := ac.setInitialTLSCertificates(certs); err != nil {
return nil, err
}
ac.logger.Info("automatically upgraded to TLS")
return ac.config, nil
default:
return ac.config, nil
}
} }
// introToken is responsible for determining the correct intro token to use // introToken is responsible for determining the correct intro token to use
@ -217,118 +202,45 @@ func (ac *AutoConfig) introToken() (string, error) {
return token, nil return token, nil
} }
// serverHosts is responsible for taking the list of server addresses and // recordInitialConfiguration is responsible for recording the AutoConfigResponse from
// resolving any go-discover provider invocations. It will then return a list // the AutoConfig.InitialConfiguration RPC. It is an all-in-one function to do the following
// of hosts. These might be hostnames and is expected that DNS resolution may // * update the Agent token in the token store
// be performed after this function runs. Additionally these may contain ports func (ac *AutoConfig) recordInitialConfiguration(resp *pbautoconf.AutoConfigResponse) error {
// so SplitHostPort could also be necessary. ac.autoConfigResponse = resp
func (ac *AutoConfig) serverHosts() ([]string, error) {
servers := ac.config.AutoConfig.ServerAddresses
providers := make(map[string]discover.Provider) ac.autoConfigSource = config.LiteralSource{
for k, v := range discover.Providers { Name: autoConfigFileName,
providers[k] = v Config: translateConfig(resp.Config),
} }
providers["k8s"] = &discoverk8s.Provider{}
disco, err := discover.New(
discover.WithUserAgent(lib.UserAgent()),
discover.WithProviders(providers),
)
// we need to re-read the configuration to determine what the correct ACL
// token to push into the token store is. Any user provided token will override
// any AutoConfig generated token.
config, err := ac.ReadConfig()
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to create go-discover resolver: %w", err) return fmt.Errorf("failed to fully resolve configuration: %w", err)
} }
var addrs []string // ignoring the return value which would indicate a change in the token
for _, addr := range servers { _ = ac.acConfig.Tokens.UpdateAgentToken(config.ACLTokens.ACLAgentToken, token.TokenSourceConfig)
switch {
case strings.Contains(addr, "provider="):
resolved, err := disco.Addrs(addr, ac.logger.StandardLogger(&hclog.StandardLoggerOptions{InferLevels: true}))
if err != nil {
ac.logger.Error("failed to resolve go-discover auto-config servers", "configuration", addr, "err", err)
continue
}
addrs = append(addrs, resolved...) // extra a structs.SignedResponse from the AutoConfigResponse for use in cache prepopulation
ac.logger.Debug("discovered auto-config servers", "servers", resolved) signed, err := extractSignedResponse(resp)
default:
addrs = append(addrs, addr)
}
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no auto-config server addresses available for use")
}
return addrs, nil
}
// resolveHost will take a single host string and convert it to a list of TCPAddrs
// This will process any port in the input as well as looking up the hostname using
// normal DNS resolution.
func (ac *AutoConfig) resolveHost(hostPort string) []net.TCPAddr {
port := ac.config.ServerPort
host, portStr, err := net.SplitHostPort(hostPort)
if err != nil { if err != nil {
if strings.Contains(err.Error(), "missing port in address") { return fmt.Errorf("failed to extract certificates from the auto-config response: %w", err)
host = hostPort
} else {
ac.logger.Warn("error splitting host address into IP and port", "address", hostPort, "error", err)
return nil
}
} else {
port, err = strconv.Atoi(portStr)
if err != nil {
ac.logger.Warn("Parsed port is not an integer", "port", portStr, "error", err)
return nil
}
} }
// resolve the host to a list of IPs // prepopulate the cache
ips, err := net.LookupIP(host) if err = ac.populateCertificateCache(signed); err != nil {
if err != nil { return fmt.Errorf("failed to populate the cache with certificate responses: %w", err)
ac.logger.Warn("IP resolution failed", "host", host, "error", err)
return nil
} }
var addrs []net.TCPAddr // update the TLS configurator with the latest certificates
for _, ip := range ips { if err := ac.updateTLSFromResponse(resp); err != nil {
addrs = append(addrs, net.TCPAddr{IP: ip, Port: port})
}
return addrs
}
// recordResponse takes an AutoConfig RPC response records it with the agent
// This will persist the configuration to disk (unless in dev mode running without
// a data dir) and will reload the configuration.
func (ac *AutoConfig) recordResponse(resp *pbautoconf.AutoConfigResponse) error {
serialized, err := pbMarshaler.MarshalToString(resp)
if err != nil {
return fmt.Errorf("failed to encode auto-config response as JSON: %w", err)
}
if err := ac.update(resp); err != nil {
return err return err
} }
// now that we know the configuration is generally fine including TLS certs go ahead and persist it to disk. return ac.persistAutoConfig(resp)
if ac.config.DataDir == "" {
ac.logger.Debug("not persisting auto-config settings because there is no data directory")
return nil
}
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
err = ioutil.WriteFile(path, []byte(serialized), 0660)
if err != nil {
return fmt.Errorf("failed to write auto-config configurations: %w", err)
}
ac.logger.Debug("auto-config settings were persisted to disk")
return nil
} }
// getInitialConfigurationOnce will perform full server to TCPAddr resolution and // getInitialConfigurationOnce will perform full server to TCPAddr resolution and
@ -352,7 +264,7 @@ func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context, csr strin
var resp pbautoconf.AutoConfigResponse var resp pbautoconf.AutoConfigResponse
servers, err := ac.serverHosts() servers, err := ac.autoConfigHosts()
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -369,6 +281,7 @@ func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context, csr strin
ac.logger.Error("AutoConfig.InitialConfiguration RPC failed", "addr", addr.String(), "error", err) ac.logger.Error("AutoConfig.InitialConfiguration RPC failed", "addr", addr.String(), "error", err)
continue continue
} }
ac.logger.Debug("AutoConfig.InitialConfiguration RPC was successful")
// update the Certificate with the private key we generated locally // update the Certificate with the private key we generated locally
if resp.Certificate != nil { if resp.Certificate != nil {
@ -379,17 +292,17 @@ func (ac *AutoConfig) getInitialConfigurationOnce(ctx context.Context, csr strin
} }
} }
return nil, ctx.Err() return nil, fmt.Errorf("No server successfully responded to the auto-config request")
} }
// getInitialConfiguration implements a loop to retry calls to getInitialConfigurationOnce. // getInitialConfiguration implements a loop to retry calls to getInitialConfigurationOnce.
// It uses the RetryWaiter on the AutoConfig object to control how often to attempt // It uses the RetryWaiter on the AutoConfig object to control how often to attempt
// the initial configuration process. It is also canceallable by cancelling the provided context. // the initial configuration process. It is also canceallable by cancelling the provided context.
func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error { func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) (*pbautoconf.AutoConfigResponse, error) {
// generate a CSR // generate a CSR
csr, key, err := ac.generateCSR() csr, key, err := ac.generateCSR()
if err != nil { if err != nil {
return err return nil, err
} }
// this resets the failures so that we will perform immediate request // this resets the failures so that we will perform immediate request
@ -397,183 +310,95 @@ func (ac *AutoConfig) getInitialConfiguration(ctx context.Context) error {
for { for {
select { select {
case <-wait: case <-wait:
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key) if resp, err := ac.getInitialConfigurationOnce(ctx, csr, key); err == nil && resp != nil {
if resp != nil { return resp, nil
return ac.recordResponse(resp)
} else if err != nil { } else if err != nil {
ac.logger.Error(err.Error()) ac.logger.Error(err.Error())
} else { } else {
ac.logger.Error("No error returned when fetching the initial auto-configuration but no response was either") ac.logger.Error("No error returned when fetching configuration from the servers but no response was either")
} }
wait = ac.acConfig.Waiter.Failed() wait = ac.acConfig.Waiter.Failed()
case <-ctx.Done(): case <-ctx.Done():
ac.logger.Info("interrupted during initial auto configuration", "err", ctx.Err()) ac.logger.Info("interrupted during initial auto configuration", "err", ctx.Err())
return ctx.Err() return nil, ctx.Err()
} }
} }
} }
// generateCSR will generate a CSR for an Agent certificate. This should
// be sent along with the AutoConfig.InitialConfiguration RPC. The generated
// CSR does NOT have a real trust domain as when generating this we do
// not yet have the CA roots. The server will update the trust domain
// for us though.
func (ac *AutoConfig) generateCSR() (csr string, key string, err error) {
// We don't provide the correct host here, because we don't know any
// better at this point. Apart from the domain, we would need the
// ClusterID, which we don't have. This is why we go with
// dummyTrustDomain the first time. Subsequent CSRs will have the
// correct TrustDomain.
id := &connect.SpiffeIDAgent{
// will be replaced
Host: dummyTrustDomain,
Datacenter: ac.config.Datacenter,
Agent: ac.config.NodeName,
}
caConfig, err := ac.config.ConnectCAConfiguration()
if err != nil {
return "", "", fmt.Errorf("Cannot generate CSR: %w", err)
}
conf, err := caConfig.GetCommonConfig()
if err != nil {
return "", "", fmt.Errorf("Failed to load common CA configuration: %w", err)
}
if conf.PrivateKeyType == "" {
conf.PrivateKeyType = connect.DefaultPrivateKeyType
}
if conf.PrivateKeyBits == 0 {
conf.PrivateKeyBits = connect.DefaultPrivateKeyBits
}
// Create a new private key
pk, pkPEM, err := connect.GeneratePrivateKeyWithConfig(conf.PrivateKeyType, conf.PrivateKeyBits)
if err != nil {
return "", "", fmt.Errorf("Failed to generate private key: %w", err)
}
dnsNames := append([]string{"localhost"}, ac.config.AutoConfig.DNSSANs...)
ipAddresses := append([]net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::")}, ac.config.AutoConfig.IPSANs...)
// Create a CSR.
//
// The Common Name includes the dummy trust domain for now but Server will
// override this when it is signed anyway so it's OK.
cn := connect.AgentCN(ac.config.NodeName, dummyTrustDomain)
csr, err = connect.CreateCSR(id, cn, pk, dnsNames, ipAddresses)
if err != nil {
return "", "", err
}
return csr, pkPEM, nil
}
// update will take an AutoConfigResponse and do all things necessary
// to restore those settings. This currently involves updating the
// config data to be used during a call to ReadConfig, updating the
// tls Configurator and prepopulating the cache.
func (ac *AutoConfig) update(resp *pbautoconf.AutoConfigResponse) error {
ac.autoConfigResponse = resp
ac.autoConfigSource = config.LiteralSource{
Name: autoConfigFileName,
Config: translateConfig(resp.Config),
}
if err := ac.updateTLSFromResponse(resp); err != nil {
return err
}
return nil
}
// updateTLSFromResponse will update the TLS certificate and roots in the shared
// TLS configurator.
func (ac *AutoConfig) updateTLSFromResponse(resp *pbautoconf.AutoConfigResponse) error {
if ac.certMonitor == nil {
return nil
}
roots, err := translateCARootsToStructs(resp.CARoots)
if err != nil {
return err
}
cert, err := translateIssuedCertToStructs(resp.Certificate)
if err != nil {
return err
}
update := &structs.SignedResponse{
IssuedCert: *cert,
ConnectCARoots: *roots,
ManualCARoots: resp.ExtraCACertificates,
}
if resp.Config != nil && resp.Config.TLS != nil {
update.VerifyServerHostname = resp.Config.TLS.VerifyServerHostname
}
if err := ac.certMonitor.Update(update); err != nil {
return fmt.Errorf("failed to update the certificate monitor: %w", err)
}
return nil
}
func (ac *AutoConfig) Start(ctx context.Context) error { func (ac *AutoConfig) Start(ctx context.Context) error {
if ac.certMonitor == nil { ac.Lock()
defer ac.Unlock()
if !ac.config.AutoConfig.Enabled && !ac.config.AutoEncryptTLS {
return nil return nil
} }
if !ac.config.AutoConfig.Enabled { if ac.running || ac.cancel != nil {
return nil return fmt.Errorf("AutoConfig is already running")
} }
_, err := ac.certMonitor.Start(ctx) // create the top level context to control the go
return err // routine executing the `run` method
ctx, cancel := context.WithCancel(ctx)
// create the channel to get cache update events through
// really we should only ever get 10 updates
ac.cacheUpdates = make(chan cache.UpdateEvent, 10)
// setup the cache watches
cancelCertWatches, err := ac.setupCertificateCacheWatches(ctx)
if err != nil {
cancel()
return fmt.Errorf("error setting up cache watches: %w", err)
}
// start the token update notifier
ac.tokenUpdates = ac.acConfig.Tokens.Notify(token.TokenKindAgent)
// store the cancel funcs
ac.cancel = cancel
ac.cancelWatches = cancelCertWatches
ac.running = true
ac.done = make(chan struct{})
go ac.run(ctx, ac.done)
ac.logger.Info("auto-config started")
return nil
}
func (ac *AutoConfig) Done() <-chan struct{} {
ac.Lock()
defer ac.Unlock()
if ac.done != nil {
return ac.done
}
// return a closed channel to indicate that we are already done
done := make(chan struct{})
close(done)
return done
}
func (ac *AutoConfig) IsRunning() bool {
ac.Lock()
defer ac.Unlock()
return ac.running
} }
func (ac *AutoConfig) Stop() bool { func (ac *AutoConfig) Stop() bool {
if ac.certMonitor == nil { ac.Lock()
defer ac.Unlock()
if !ac.running {
return false return false
} }
if !ac.config.AutoConfig.Enabled { if ac.cancel != nil {
return false ac.cancel()
} }
return ac.certMonitor.Stop() return true
}
func (ac *AutoConfig) FallbackTLS(ctx context.Context) (*structs.SignedResponse, error) {
// generate a CSR
csr, key, err := ac.generateCSR()
if err != nil {
return nil, err
}
resp, err := ac.getInitialConfigurationOnce(ctx, csr, key)
if err != nil {
return nil, err
}
return extractSignedResponse(resp)
}
func (ac *AutoConfig) RecordUpdatedCerts(resp *structs.SignedResponse) error {
var err error
ac.autoConfigResponse.ExtraCACertificates = resp.ManualCARoots
ac.autoConfigResponse.CARoots, err = translateCARootsToProtobuf(&resp.ConnectCARoots)
if err != nil {
return err
}
ac.autoConfigResponse.Certificate, err = translateIssuedCertToProtobuf(&resp.IssuedCert)
if err != nil {
return err
}
return ac.recordResponse(ac.autoConfigResponse)
} }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,111 @@
package autoconf
import (
"context"
"fmt"
"net"
"strings"
"github.com/hashicorp/consul/agent/structs"
)
func (ac *AutoConfig) autoEncryptInitialCerts(ctx context.Context) (*structs.SignedResponse, error) {
// generate a CSR
csr, key, err := ac.generateCSR()
if err != nil {
return nil, err
}
// this resets the failures so that we will perform immediate request
wait := ac.acConfig.Waiter.Success()
for {
select {
case <-wait:
if resp, err := ac.autoEncryptInitialCertsOnce(ctx, csr, key); err == nil && resp != nil {
return resp, nil
} else if err != nil {
ac.logger.Error(err.Error())
} else {
ac.logger.Error("No error returned when fetching certificates from the servers but no response was either")
}
wait = ac.acConfig.Waiter.Failed()
case <-ctx.Done():
ac.logger.Info("interrupted during retrieval of auto-encrypt certificates", "err", ctx.Err())
return nil, ctx.Err()
}
}
}
func (ac *AutoConfig) autoEncryptInitialCertsOnce(ctx context.Context, csr, key string) (*structs.SignedResponse, error) {
request := structs.CASignRequest{
WriteRequest: structs.WriteRequest{Token: ac.acConfig.Tokens.AgentToken()},
Datacenter: ac.config.Datacenter,
CSR: csr,
}
var resp structs.SignedResponse
servers, err := ac.autoEncryptHosts()
if err != nil {
return nil, err
}
for _, s := range servers {
// try each IP to see if we can successfully make the request
for _, addr := range ac.resolveHost(s) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
ac.logger.Debug("making AutoEncrypt.Sign RPC", "addr", addr.String())
err = ac.acConfig.DirectRPC.RPC(ac.config.Datacenter, ac.config.NodeName, &addr, "AutoEncrypt.Sign", &request, &resp)
if err != nil {
ac.logger.Error("AutoEncrypt.Sign RPC failed", "addr", addr.String(), "error", err)
continue
}
resp.IssuedCert.PrivateKeyPEM = key
return &resp, nil
}
}
return nil, fmt.Errorf("No servers successfully responded to the auto-encrypt request")
}
func (ac *AutoConfig) autoEncryptHosts() ([]string, error) {
// use servers known to gossip if there are any
if ac.acConfig.ServerProvider != nil {
if srv := ac.acConfig.ServerProvider.FindLANServer(); srv != nil {
return []string{srv.Addr.String()}, nil
}
}
hosts, err := ac.discoverServers(ac.config.RetryJoinLAN)
if err != nil {
return nil, err
}
var addrs []string
// The addresses we use for auto-encrypt are the retry join and start join
// addresses. These are for joining serf and therefore we cannot rely on the
// ports for these. This loop strips any port that may have been specified and
// will let subsequent resolveAddr calls add on the default RPC port.
for _, addr := range append(ac.config.StartJoinAddrsLAN, hosts...) {
host, _, err := net.SplitHostPort(addr)
if err != nil {
if strings.Contains(err.Error(), "missing port in address") {
host = addr
} else {
ac.logger.Warn("error splitting host address into IP and port", "address", addr, "error", err)
continue
}
}
addrs = append(addrs, host)
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no auto-encrypt server addresses available for use")
}
return addrs, nil
}

View File

@ -0,0 +1,562 @@
package autoconf
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"fmt"
"net"
"net/url"
"testing"
"time"
"github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestAutoEncrypt_generateCSR(t *testing.T) {
type testCase struct {
conf *config.RuntimeConfig
// to validate the csr
expectedSubject pkix.Name
expectedSigAlg x509.SignatureAlgorithm
expectedPubAlg x509.PublicKeyAlgorithm
expectedDNSNames []string
expectedIPs []net.IP
expectedURIs []*url.URL
}
cases := map[string]testCase{
"ip-sans": {
conf: &config.RuntimeConfig{
Datacenter: "dc1",
NodeName: "test-node",
AutoEncryptTLS: true,
AutoEncryptIPSAN: []net.IP{net.IPv4(198, 18, 0, 1), net.IPv4(198, 18, 0, 2)},
},
expectedSubject: pkix.Name{
CommonName: connect.AgentCN("test-node", unknownTrustDomain),
Names: []pkix.AttributeTypeAndValue{
{
// 2,5,4,3 is the CommonName type ASN1 identifier
Type: asn1.ObjectIdentifier{2, 5, 4, 3},
Value: "testnode.agnt.unknown.consul",
},
},
},
expectedSigAlg: x509.ECDSAWithSHA256,
expectedPubAlg: x509.ECDSA,
expectedDNSNames: defaultDNSSANs,
expectedIPs: append(defaultIPSANs,
net.IP{198, 18, 0, 1},
net.IP{198, 18, 0, 2},
),
expectedURIs: []*url.URL{
{
Scheme: "spiffe",
Host: unknownTrustDomain,
Path: "/agent/client/dc/dc1/id/test-node",
},
},
},
"dns-sans": {
conf: &config.RuntimeConfig{
Datacenter: "dc1",
NodeName: "test-node",
AutoEncryptTLS: true,
AutoEncryptDNSSAN: []string{"foo.local", "bar.local"},
},
expectedSubject: pkix.Name{
CommonName: connect.AgentCN("test-node", unknownTrustDomain),
Names: []pkix.AttributeTypeAndValue{
{
// 2,5,4,3 is the CommonName type ASN1 identifier
Type: asn1.ObjectIdentifier{2, 5, 4, 3},
Value: "testnode.agnt.unknown.consul",
},
},
},
expectedSigAlg: x509.ECDSAWithSHA256,
expectedPubAlg: x509.ECDSA,
expectedDNSNames: append(defaultDNSSANs, "foo.local", "bar.local"),
expectedIPs: defaultIPSANs,
expectedURIs: []*url.URL{
{
Scheme: "spiffe",
Host: unknownTrustDomain,
Path: "/agent/client/dc/dc1/id/test-node",
},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ac := AutoConfig{config: tcase.conf}
csr, _, err := ac.generateCSR()
require.NoError(t, err)
request, err := connect.ParseCSR(csr)
require.NoError(t, err)
require.NotNil(t, request)
require.Equal(t, tcase.expectedSubject, request.Subject)
require.Equal(t, tcase.expectedSigAlg, request.SignatureAlgorithm)
require.Equal(t, tcase.expectedPubAlg, request.PublicKeyAlgorithm)
require.Equal(t, tcase.expectedDNSNames, request.DNSNames)
require.Equal(t, tcase.expectedIPs, request.IPAddresses)
require.Equal(t, tcase.expectedURIs, request.URIs)
})
}
}
func TestAutoEncrypt_hosts(t *testing.T) {
type testCase struct {
serverProvider ServerProvider
config *config.RuntimeConfig
hosts []string
err string
}
providerNone := newMockServerProvider(t)
providerNone.On("FindLANServer").Return(nil).Times(0)
providerWithServer := newMockServerProvider(t)
providerWithServer.On("FindLANServer").Return(&metadata.Server{Addr: &net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 1234}}).Times(0)
cases := map[string]testCase{
"router-override": {
serverProvider: providerWithServer,
config: &config.RuntimeConfig{
RetryJoinLAN: []string{"127.0.0.1:9876"},
StartJoinAddrsLAN: []string{"192.168.1.2:4321"},
},
hosts: []string{"198.18.0.1:1234"},
},
"various-addresses": {
serverProvider: providerNone,
config: &config.RuntimeConfig{
RetryJoinLAN: []string{"198.18.0.1", "foo.com", "[2001:db8::1234]:1234", "abc.local:9876"},
StartJoinAddrsLAN: []string{"192.168.1.1:5432", "start.local", "[::ffff:172.16.5.4]", "main.dev:6789"},
},
hosts: []string{
"192.168.1.1",
"start.local",
"[::ffff:172.16.5.4]",
"main.dev",
"198.18.0.1",
"foo.com",
"2001:db8::1234",
"abc.local",
},
},
"split-host-port-error": {
serverProvider: providerNone,
config: &config.RuntimeConfig{
StartJoinAddrsLAN: []string{"this-is-not:a:ip:and_port"},
},
err: "no auto-encrypt server addresses available for use",
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
ac := AutoConfig{
config: tcase.config,
logger: testutil.Logger(t),
acConfig: Config{
ServerProvider: tcase.serverProvider,
},
}
hosts, err := ac.autoEncryptHosts()
if tcase.err != "" {
testutil.RequireErrorContains(t, err, tcase.err)
} else {
require.NoError(t, err)
require.Equal(t, tcase.hosts, hosts)
}
})
}
}
func TestAutoEncrypt_InitialCerts(t *testing.T) {
token := "1a148388-3dd7-4db4-9eea-520424b4a86a"
datacenter := "foo"
nodeName := "bar"
mcfg := newMockedConfig(t)
_, indexedRoots, cert := testCerts(t, nodeName, datacenter)
// The following are called once for each round through the auto-encrypt initial certs outer loop
// (not the per-host direct rpc attempts but the one involving the RetryWaiter)
mcfg.tokens.On("AgentToken").Return(token).Times(2)
mcfg.serverProvider.On("FindLANServer").Return(nil).Times(2)
request := structs.CASignRequest{
WriteRequest: structs.WriteRequest{Token: token},
Datacenter: datacenter,
// this gets removed by the mock code as its non-deterministic what it will be
CSR: "",
}
// first failure
mcfg.directRPC.On("RPC",
datacenter,
nodeName,
&net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300},
"AutoEncrypt.Sign",
&request,
&structs.SignedResponse{},
).Once().Return(fmt.Errorf("injected error"))
// second failure
mcfg.directRPC.On("RPC",
datacenter,
nodeName,
&net.TCPAddr{IP: net.IPv4(198, 18, 0, 2), Port: 8300},
"AutoEncrypt.Sign",
&request,
&structs.SignedResponse{},
).Once().Return(fmt.Errorf("injected error"))
// third times is successfuly (second attempt to first server)
mcfg.directRPC.On("RPC",
datacenter,
nodeName,
&net.TCPAddr{IP: net.IPv4(198, 18, 0, 1), Port: 8300},
"AutoEncrypt.Sign",
&request,
&structs.SignedResponse{},
).Once().Return(nil).Run(func(args mock.Arguments) {
resp, ok := args.Get(5).(*structs.SignedResponse)
require.True(t, ok)
resp.ConnectCARoots = *indexedRoots
resp.IssuedCert = *cert
resp.VerifyServerHostname = true
})
mcfg.Config.Waiter = lib.NewRetryWaiter(2, 0, 1*time.Millisecond, nil)
ac := AutoConfig{
config: &config.RuntimeConfig{
Datacenter: datacenter,
NodeName: nodeName,
RetryJoinLAN: []string{"198.18.0.1:1234", "198.18.0.2:3456"},
ServerPort: 8300,
},
acConfig: mcfg.Config,
logger: testutil.Logger(t),
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := ac.autoEncryptInitialCerts(ctx)
require.NoError(t, err)
require.NotNil(t, resp)
require.True(t, resp.VerifyServerHostname)
require.NotEmpty(t, resp.IssuedCert.PrivateKeyPEM)
resp.IssuedCert.PrivateKeyPEM = ""
cert.PrivateKeyPEM = ""
require.Equal(t, cert, &resp.IssuedCert)
require.Equal(t, indexedRoots, &resp.ConnectCARoots)
require.Empty(t, resp.ManualCARoots)
}
func TestAutoEncrypt_InitialConfiguration(t *testing.T) {
token := "010494ae-ee45-4433-903c-a58c91297714"
nodeName := "auto-encrypt"
datacenter := "dc1"
mcfg := newMockedConfig(t)
loader := setupRuntimeConfig(t)
loader.addConfigHCL(`
auto_encrypt {
tls = true
}
`)
loader.opts.Config.NodeName = &nodeName
mcfg.Config.Loader = loader.Load
indexedRoots, cert, extraCerts := mcfg.setupInitialTLS(t, nodeName, datacenter, token)
// prepopulation is going to grab the token to populate the correct cache key
mcfg.tokens.On("AgentToken").Return(token).Times(0)
// no server provider
mcfg.serverProvider.On("FindLANServer").Return(&metadata.Server{Addr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300}}).Times(1)
populateResponse := func(args mock.Arguments) {
resp, ok := args.Get(5).(*structs.SignedResponse)
require.True(t, ok)
*resp = structs.SignedResponse{
VerifyServerHostname: true,
ConnectCARoots: *indexedRoots,
IssuedCert: *cert,
ManualCARoots: extraCerts,
}
}
expectedRequest := structs.CASignRequest{
WriteRequest: structs.WriteRequest{Token: token},
Datacenter: datacenter,
// TODO (autoconf) Maybe in the future we should populate a CSR
// and do some manual parsing/verification of the contents. The
// bits not having to do with the signing key such as the requested
// SANs and CN. For now though the mockDirectRPC type will empty
// the CSR so we have to pass in an empty string to the expectation.
CSR: "",
}
mcfg.directRPC.On(
"RPC",
datacenter,
nodeName,
&net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 8300},
"AutoEncrypt.Sign",
&expectedRequest,
&structs.SignedResponse{}).Return(nil).Run(populateResponse)
ac, err := New(mcfg.Config)
require.NoError(t, err)
require.NotNil(t, ac)
cfg, err := ac.InitialConfiguration(context.Background())
require.NoError(t, err)
require.NotNil(t, cfg)
}
func TestAutoEncrypt_TokenUpdate(t *testing.T) {
testAC := startedAutoConfig(t, true)
newToken := "1a4cc445-86ed-46b4-a355-bbf5a11dddb0"
rootsCtx, rootsCancel := context.WithCancel(context.Background())
testAC.mcfg.cache.On("Notify",
mock.Anything,
cachetype.ConnectCARootName,
&structs.DCSpecificRequest{Datacenter: testAC.ac.config.Datacenter},
rootsWatchID,
mock.Anything,
).Return(nil).Once().Run(func(args mock.Arguments) {
rootsCancel()
})
leafCtx, leafCancel := context.WithCancel(context.Background())
testAC.mcfg.cache.On("Notify",
mock.Anything,
cachetype.ConnectCALeafName,
&cachetype.ConnectCALeafRequest{
Datacenter: "dc1",
Agent: "autoconf",
Token: newToken,
DNSSAN: defaultDNSSANs,
IPSAN: defaultIPSANs,
},
leafWatchID,
mock.Anything,
).Return(nil).Once().Run(func(args mock.Arguments) {
leafCancel()
})
// this will be retrieved once when resetting the leaf cert watch
testAC.mcfg.tokens.On("AgentToken").Return(newToken).Once()
// send the notification about the token update
testAC.tokenUpdates <- struct{}{}
// wait for the leaf cert watches
require.True(t, waitForChans(100*time.Millisecond, leafCtx.Done(), rootsCtx.Done()), "New cache watches were not started within 100ms")
}
func TestAutoEncrypt_RootsUpdate(t *testing.T) {
testAC := startedAutoConfig(t, true)
secondCA := connect.TestCA(t, testAC.initialRoots.Roots[0])
secondRoots := structs.IndexedCARoots{
ActiveRootID: secondCA.ID,
TrustDomain: connect.TestClusterID,
Roots: []*structs.CARoot{
secondCA,
testAC.initialRoots.Roots[0],
},
QueryMeta: structs.QueryMeta{
Index: 99,
},
}
updatedCtx, cancel := context.WithCancel(context.Background())
testAC.mcfg.tlsCfg.On("UpdateAutoTLSCA",
[]string{secondCA.RootCert, testAC.initialRoots.Roots[0].RootCert},
).Return(nil).Once().Run(func(args mock.Arguments) {
cancel()
})
// when a cache event comes in we end up recalculating the fallback timer which requires this call
testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(time.Now().Add(10 * time.Minute)).Once()
req := structs.DCSpecificRequest{Datacenter: "dc1"}
require.True(t, testAC.mcfg.cache.sendNotification(context.Background(), req.CacheInfo().Key, cache.UpdateEvent{
CorrelationID: rootsWatchID,
Result: &secondRoots,
Meta: cache.ResultMeta{
Index: secondRoots.Index,
},
}))
require.True(t, waitForChans(100*time.Millisecond, updatedCtx.Done()), "TLS certificates were not updated within the alotted time")
}
func TestAutoEncrypt_CertUpdate(t *testing.T) {
testAC := startedAutoConfig(t, true)
secondCert := newLeaf(t, "autoconf", "dc1", testAC.initialRoots.Roots[0], 99, 10*time.Minute)
updatedCtx, cancel := context.WithCancel(context.Background())
testAC.mcfg.tlsCfg.On("UpdateAutoTLSCert",
secondCert.CertPEM,
"redacted",
).Return(nil).Once().Run(func(args mock.Arguments) {
cancel()
})
// when a cache event comes in we end up recalculating the fallback timer which requires this call
testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(secondCert.ValidBefore).Once()
req := cachetype.ConnectCALeafRequest{
Datacenter: "dc1",
Agent: "autoconf",
Token: testAC.originalToken,
DNSSAN: defaultDNSSANs,
IPSAN: defaultIPSANs,
}
require.True(t, testAC.mcfg.cache.sendNotification(context.Background(), req.CacheInfo().Key, cache.UpdateEvent{
CorrelationID: leafWatchID,
Result: secondCert,
Meta: cache.ResultMeta{
Index: secondCert.ModifyIndex,
},
}))
require.True(t, waitForChans(100*time.Millisecond, updatedCtx.Done()), "TLS certificates were not updated within the alotted time")
}
func TestAutoEncrypt_Fallback(t *testing.T) {
testAC := startedAutoConfig(t, true)
// at this point everything is operating normally and we are just
// waiting for events. We are going to send a new cert that is basically
// already expired and then allow the fallback routine to kick in.
secondCert := newLeaf(t, "autoconf", "dc1", testAC.initialRoots.Roots[0], 100, time.Nanosecond)
secondCA := connect.TestCA(t, testAC.initialRoots.Roots[0])
secondRoots := structs.IndexedCARoots{
ActiveRootID: secondCA.ID,
TrustDomain: connect.TestClusterID,
Roots: []*structs.CARoot{
secondCA,
testAC.initialRoots.Roots[0],
},
QueryMeta: structs.QueryMeta{
Index: 101,
},
}
thirdCert := newLeaf(t, "autoconf", "dc1", secondCA, 102, 10*time.Minute)
// setup the expectation for when the certs get updated initially
updatedCtx, updateCancel := context.WithCancel(context.Background())
testAC.mcfg.tlsCfg.On("UpdateAutoTLSCert",
secondCert.CertPEM,
"redacted",
).Return(nil).Once().Run(func(args mock.Arguments) {
updateCancel()
})
// when a cache event comes in we end up recalculating the fallback timer which requires this call
testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(secondCert.ValidBefore).Once()
testAC.mcfg.tlsCfg.On("AutoEncryptCertExpired").Return(true).Once()
fallbackCtx, fallbackCancel := context.WithCancel(context.Background())
// also testing here that we can change server IPs for ongoing operations
testAC.mcfg.serverProvider.On("FindLANServer").Once().Return(&metadata.Server{
Addr: &net.TCPAddr{IP: net.IPv4(198, 18, 23, 2), Port: 8300},
})
// after sending the notification for the cert update another InitialConfiguration RPC
// will be made to pull down the latest configuration. So we need to set up the response
// for the second RPC
populateResponse := func(args mock.Arguments) {
resp, ok := args.Get(5).(*structs.SignedResponse)
require.True(t, ok)
*resp = structs.SignedResponse{
VerifyServerHostname: true,
ConnectCARoots: secondRoots,
IssuedCert: *thirdCert,
ManualCARoots: testAC.extraCerts,
}
fallbackCancel()
}
expectedRequest := structs.CASignRequest{
WriteRequest: structs.WriteRequest{Token: testAC.originalToken},
Datacenter: "dc1",
// TODO (autoconf) Maybe in the future we should populate a CSR
// and do some manual parsing/verification of the contents. The
// bits not having to do with the signing key such as the requested
// SANs and CN. For now though the mockDirectRPC type will empty
// the CSR so we have to pass in an empty string to the expectation.
CSR: "",
}
// the fallback routine to perform auto-encrypt again will need to grab this
testAC.mcfg.tokens.On("AgentToken").Return(testAC.originalToken).Once()
testAC.mcfg.directRPC.On(
"RPC",
"dc1",
"autoconf",
&net.TCPAddr{IP: net.IPv4(198, 18, 23, 2), Port: 8300},
"AutoEncrypt.Sign",
&expectedRequest,
&structs.SignedResponse{}).Return(nil).Run(populateResponse).Once()
testAC.mcfg.expectInitialTLS(t, "autoconf", "dc1", testAC.originalToken, secondCA, &secondRoots, thirdCert, testAC.extraCerts)
// after the second RPC we now will use the new certs validity period in the next run loop iteration
testAC.mcfg.tlsCfg.On("AutoEncryptCertNotAfter").Return(time.Now().Add(10 * time.Minute)).Once()
// now that all the mocks are set up we can trigger the whole thing by sending the second expired cert
// as a cache update event.
req := cachetype.ConnectCALeafRequest{
Datacenter: "dc1",
Agent: "autoconf",
Token: testAC.originalToken,
DNSSAN: defaultDNSSANs,
IPSAN: defaultIPSANs,
}
require.True(t, testAC.mcfg.cache.sendNotification(context.Background(), req.CacheInfo().Key, cache.UpdateEvent{
CorrelationID: leafWatchID,
Result: secondCert,
Meta: cache.ResultMeta{
Index: secondCert.ModifyIndex,
},
}))
// wait for the TLS certificates to get updated
require.True(t, waitForChans(100*time.Millisecond, updatedCtx.Done()), "TLS certificates were not updated within the alotted time")
// now wait for the fallback routine to be invoked
require.True(t, waitForChans(100*time.Millisecond, fallbackCtx.Done()), "fallback routines did not get invoked within the alotted time")
}

View File

@ -3,9 +3,12 @@ package autoconf
import ( import (
"context" "context"
"net" "net"
"time"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
) )
@ -18,12 +21,35 @@ type DirectRPC interface {
RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error
} }
// CertMonitor is the interface that needs to be satisfied for AutoConfig to be able to // Cache is an interface to represent the methods of the
// setup monitoring of the Connect TLS certificate after we first get it. // agent/cache.Cache struct that we care about
type CertMonitor interface { type Cache interface {
Update(*structs.SignedResponse) error Notify(ctx context.Context, t string, r cache.Request, correlationID string, ch chan<- cache.UpdateEvent) error
Start(context.Context) (<-chan struct{}, error) Prepopulate(t string, result cache.FetchResult, dc string, token string, key string) error
Stop() bool }
// ServerProvider is an interface that can be used to find one server in the local DC known to
// the agent via Gossip
type ServerProvider interface {
FindLANServer() *metadata.Server
}
// TLSConfigurator is an interface of the methods on the tlsutil.Configurator that we will require at
// runtime.
type TLSConfigurator interface {
UpdateAutoTLS(manualCAPEMs, connectCAPEMs []string, pub, priv string, verifyServerHostname bool) error
UpdateAutoTLSCA([]string) error
UpdateAutoTLSCert(pub, priv string) error
AutoEncryptCertNotAfter() time.Time
AutoEncryptCertExpired() bool
}
// TokenStore is an interface of the methods we will need to use from the token.Store.
type TokenStore interface {
AgentToken() string
UpdateAgentToken(secret string, source token.TokenSource) bool
Notify(kind token.TokenKind) token.Notifier
StopNotify(notifier token.Notifier)
} }
// Config contains all the tunables for AutoConfig // Config contains all the tunables for AutoConfig
@ -37,6 +63,10 @@ type Config struct {
// configuration. Setting this field is required. // configuration. Setting this field is required.
DirectRPC DirectRPC DirectRPC DirectRPC
// ServerProvider is the interfaced to be used by AutoConfig to find any
// known servers during fallback operations.
ServerProvider ServerProvider
// Waiter is a RetryWaiter to be used during retrieval of the // Waiter is a RetryWaiter to be used during retrieval of the
// initial configuration. When a round of requests fails we will // initial configuration. When a round of requests fails we will
// wait and eventually make another round of requests (1 round // wait and eventually make another round of requests (1 round
@ -49,14 +79,28 @@ type Config struct {
// having the test take minutes/hours to complete. // having the test take minutes/hours to complete.
Waiter *lib.RetryWaiter Waiter *lib.RetryWaiter
// CertMonitor is the Connect TLS Certificate Monitor to be used for ongoing
// certificate renewals and connect CA roots updates. This field is not
// strictly required but if not provided the TLS certificates retrieved
// through by the AutoConfig.InitialConfiguration RPC will not be used
// or renewed.
CertMonitor CertMonitor
// Loader merges source with the existing FileSources and returns the complete // Loader merges source with the existing FileSources and returns the complete
// RuntimeConfig. // RuntimeConfig.
Loader func(source config.Source) (cfg *config.RuntimeConfig, warnings []string, err error) Loader func(source config.Source) (cfg *config.RuntimeConfig, warnings []string, err error)
// TLSConfigurator is the shared TLS Configurator. AutoConfig will update the
// auto encrypt/auto config certs as they are renewed.
TLSConfigurator TLSConfigurator
// Cache is an object implementing our Cache interface. The Cache
// used at runtime must be able to handle Roots and Leaf Cert watches
Cache Cache
// FallbackLeeway is the amount of time after certificate expiration before
// invoking the fallback routine. If not set this will default to 10s.
FallbackLeeway time.Duration
// FallbackRetry is the duration between Fallback invocations when the configured
// fallback routine returns an error. If not set this will default to 1m.
FallbackRetry time.Duration
// Tokens is the shared token store. It is used to retrieve the current
// agent token as well as getting notifications when that token is updated.
// This field is required.
Tokens TokenStore
} }

View File

@ -22,9 +22,9 @@ import (
// package cannot import the agent/config package without running into import cycles. // package cannot import the agent/config package without running into import cycles.
func translateConfig(c *pbconfig.Config) config.Config { func translateConfig(c *pbconfig.Config) config.Config {
result := config.Config{ result := config.Config{
Datacenter: &c.Datacenter, Datacenter: stringPtrOrNil(c.Datacenter),
PrimaryDatacenter: &c.PrimaryDatacenter, PrimaryDatacenter: stringPtrOrNil(c.PrimaryDatacenter),
NodeName: &c.NodeName, NodeName: stringPtrOrNil(c.NodeName),
// only output the SegmentName in the configuration if its non-empty // only output the SegmentName in the configuration if its non-empty
// this will avoid a warning later when parsing the persisted configuration // this will avoid a warning later when parsing the persisted configuration
SegmentName: stringPtrOrNil(c.SegmentName), SegmentName: stringPtrOrNil(c.SegmentName),
@ -42,13 +42,13 @@ func translateConfig(c *pbconfig.Config) config.Config {
if a := c.ACL; a != nil { if a := c.ACL; a != nil {
result.ACL = config.ACL{ result.ACL = config.ACL{
Enabled: &a.Enabled, Enabled: &a.Enabled,
PolicyTTL: &a.PolicyTTL, PolicyTTL: stringPtrOrNil(a.PolicyTTL),
RoleTTL: &a.RoleTTL, RoleTTL: stringPtrOrNil(a.RoleTTL),
TokenTTL: &a.TokenTTL, TokenTTL: stringPtrOrNil(a.TokenTTL),
DownPolicy: &a.DownPolicy, DownPolicy: stringPtrOrNil(a.DownPolicy),
DefaultPolicy: &a.DefaultPolicy, DefaultPolicy: stringPtrOrNil(a.DefaultPolicy),
EnableKeyListPolicy: &a.EnableKeyListPolicy, EnableKeyListPolicy: &a.EnableKeyListPolicy,
DisabledTTL: &a.DisabledTTL, DisabledTTL: stringPtrOrNil(a.DisabledTTL),
EnableTokenPersistence: &a.EnableTokenPersistence, EnableTokenPersistence: &a.EnableTokenPersistence,
} }
@ -76,7 +76,7 @@ func translateConfig(c *pbconfig.Config) config.Config {
result.RetryJoinLAN = g.RetryJoinLAN result.RetryJoinLAN = g.RetryJoinLAN
if e := c.Gossip.Encryption; e != nil { if e := c.Gossip.Encryption; e != nil {
result.EncryptKey = &e.Key result.EncryptKey = stringPtrOrNil(e.Key)
result.EncryptVerifyIncoming = &e.VerifyIncoming result.EncryptVerifyIncoming = &e.VerifyIncoming
result.EncryptVerifyOutgoing = &e.VerifyOutgoing result.EncryptVerifyOutgoing = &e.VerifyOutgoing
} }

View File

@ -1,10 +1,13 @@
package autoconf package autoconf
import ( import (
"fmt"
"testing" "testing"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/structs"
pbconfig "github.com/hashicorp/consul/proto/pbconfig" pbconfig "github.com/hashicorp/consul/proto/pbconfig"
"github.com/hashicorp/consul/proto/pbconnect"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -16,6 +19,38 @@ func boolPointer(b bool) *bool {
return &b return &b
} }
func translateCARootToProtobuf(in *structs.CARoot) (*pbconnect.CARoot, error) {
var out pbconnect.CARoot
if err := mapstructureTranslateToProtobuf(in, &out); err != nil {
return nil, fmt.Errorf("Failed to re-encode CA Roots: %w", err)
}
return &out, nil
}
func mustTranslateCARootToProtobuf(t *testing.T, in *structs.CARoot) *pbconnect.CARoot {
out, err := translateCARootToProtobuf(in)
require.NoError(t, err)
return out
}
func mustTranslateCARootsToStructs(t *testing.T, in *pbconnect.CARoots) *structs.IndexedCARoots {
out, err := translateCARootsToStructs(in)
require.NoError(t, err)
return out
}
func mustTranslateCARootsToProtobuf(t *testing.T, in *structs.IndexedCARoots) *pbconnect.CARoots {
out, err := translateCARootsToProtobuf(in)
require.NoError(t, err)
return out
}
func mustTranslateIssuedCertToProtobuf(t *testing.T, in *structs.IssuedCert) *pbconnect.IssuedCert {
out, err := translateIssuedCertToProtobuf(in)
require.NoError(t, err)
return out
}
func TestTranslateConfig(t *testing.T) { func TestTranslateConfig(t *testing.T) {
original := pbconfig.Config{ original := pbconfig.Config{
Datacenter: "abc", Datacenter: "abc",
@ -119,3 +154,9 @@ func TestTranslateConfig(t *testing.T) {
translated := translateConfig(&original) translated := translateConfig(&original)
require.Equal(t, expected, translated) require.Equal(t, expected, translated)
} }
func TestCArootsTranslation(t *testing.T) {
_, indexedRoots, _ := testCerts(t, "autoconf", "dc1")
protoRoots := mustTranslateCARootsToProtobuf(t, indexedRoots)
require.Equal(t, indexedRoots, mustTranslateCARootsToStructs(t, protoRoots))
}

View File

@ -0,0 +1,337 @@
package autoconf
import (
"context"
"net"
"sync"
"testing"
"time"
"github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/proto/pbautoconf"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/mock"
)
type mockDirectRPC struct {
mock.Mock
}
func newMockDirectRPC(t *testing.T) *mockDirectRPC {
m := mockDirectRPC{}
m.Test(t)
return &m
}
func (m *mockDirectRPC) RPC(dc string, node string, addr net.Addr, method string, args interface{}, reply interface{}) error {
var retValues mock.Arguments
if method == "AutoConfig.InitialConfiguration" {
req := args.(*pbautoconf.AutoConfigRequest)
csr := req.CSR
req.CSR = ""
retValues = m.Called(dc, node, addr, method, args, reply)
req.CSR = csr
} else if method == "AutoEncrypt.Sign" {
req := args.(*structs.CASignRequest)
csr := req.CSR
req.CSR = ""
retValues = m.Called(dc, node, addr, method, args, reply)
req.CSR = csr
} else {
retValues = m.Called(dc, node, addr, method, args, reply)
}
return retValues.Error(0)
}
type mockTLSConfigurator struct {
mock.Mock
}
func newMockTLSConfigurator(t *testing.T) *mockTLSConfigurator {
m := mockTLSConfigurator{}
m.Test(t)
return &m
}
func (m *mockTLSConfigurator) UpdateAutoTLS(manualCAPEMs, connectCAPEMs []string, pub, priv string, verifyServerHostname bool) error {
if priv != "" {
priv = "redacted"
}
ret := m.Called(manualCAPEMs, connectCAPEMs, pub, priv, verifyServerHostname)
return ret.Error(0)
}
func (m *mockTLSConfigurator) UpdateAutoTLSCA(pems []string) error {
ret := m.Called(pems)
return ret.Error(0)
}
func (m *mockTLSConfigurator) UpdateAutoTLSCert(pub, priv string) error {
if priv != "" {
priv = "redacted"
}
ret := m.Called(pub, priv)
return ret.Error(0)
}
func (m *mockTLSConfigurator) AutoEncryptCertNotAfter() time.Time {
ret := m.Called()
ts, _ := ret.Get(0).(time.Time)
return ts
}
func (m *mockTLSConfigurator) AutoEncryptCertExpired() bool {
ret := m.Called()
return ret.Bool(0)
}
type mockServerProvider struct {
mock.Mock
}
func newMockServerProvider(t *testing.T) *mockServerProvider {
m := mockServerProvider{}
m.Test(t)
return &m
}
func (m *mockServerProvider) FindLANServer() *metadata.Server {
ret := m.Called()
srv, _ := ret.Get(0).(*metadata.Server)
return srv
}
type mockWatcher struct {
ch chan<- cache.UpdateEvent
done <-chan struct{}
}
type mockCache struct {
mock.Mock
lock sync.Mutex
watchers map[string][]mockWatcher
}
func newMockCache(t *testing.T) *mockCache {
m := mockCache{
watchers: make(map[string][]mockWatcher),
}
m.Test(t)
return &m
}
func (m *mockCache) Notify(ctx context.Context, t string, r cache.Request, correlationID string, ch chan<- cache.UpdateEvent) error {
ret := m.Called(ctx, t, r, correlationID, ch)
err := ret.Error(0)
if err == nil {
m.lock.Lock()
key := r.CacheInfo().Key
m.watchers[key] = append(m.watchers[key], mockWatcher{ch: ch, done: ctx.Done()})
m.lock.Unlock()
}
return err
}
func (m *mockCache) Prepopulate(t string, result cache.FetchResult, dc string, token string, key string) error {
var restore string
cert, ok := result.Value.(*structs.IssuedCert)
if ok {
// we cannot know what the private key is prior to it being injected into the cache.
// therefore redact it here and all mock expectations should take that into account
restore = cert.PrivateKeyPEM
cert.PrivateKeyPEM = "redacted"
}
ret := m.Called(t, result, dc, token, key)
if ok && restore != "" {
cert.PrivateKeyPEM = restore
}
return ret.Error(0)
}
func (m *mockCache) sendNotification(ctx context.Context, key string, u cache.UpdateEvent) bool {
m.lock.Lock()
defer m.lock.Unlock()
watchers, ok := m.watchers[key]
if !ok || len(m.watchers) < 1 {
return false
}
var newWatchers []mockWatcher
for _, watcher := range watchers {
select {
case watcher.ch <- u:
newWatchers = append(newWatchers, watcher)
case <-watcher.done:
// do nothing, this watcher will be removed from the list
case <-ctx.Done():
// return doesn't matter here really, the test is being cancelled
return true
}
}
// this removes any already cancelled watches from being sent to
m.watchers[key] = newWatchers
return true
}
type mockTokenStore struct {
mock.Mock
}
func newMockTokenStore(t *testing.T) *mockTokenStore {
m := mockTokenStore{}
m.Test(t)
return &m
}
func (m *mockTokenStore) AgentToken() string {
ret := m.Called()
return ret.String(0)
}
func (m *mockTokenStore) UpdateAgentToken(secret string, source token.TokenSource) bool {
return m.Called(secret, source).Bool(0)
}
func (m *mockTokenStore) Notify(kind token.TokenKind) token.Notifier {
ret := m.Called(kind)
n, _ := ret.Get(0).(token.Notifier)
return n
}
func (m *mockTokenStore) StopNotify(notifier token.Notifier) {
m.Called(notifier)
}
type mockedConfig struct {
Config
directRPC *mockDirectRPC
serverProvider *mockServerProvider
cache *mockCache
tokens *mockTokenStore
tlsCfg *mockTLSConfigurator
}
func newMockedConfig(t *testing.T) *mockedConfig {
directRPC := newMockDirectRPC(t)
serverProvider := newMockServerProvider(t)
mcache := newMockCache(t)
tokens := newMockTokenStore(t)
tlsCfg := newMockTLSConfigurator(t)
// I am not sure it is well defined behavior but in testing it
// out it does appear like Cleanup functions can fail tests
// Adding in the mock expectations assertions here saves us
// a bunch of code in the other test functions.
t.Cleanup(func() {
if !t.Failed() {
directRPC.AssertExpectations(t)
serverProvider.AssertExpectations(t)
mcache.AssertExpectations(t)
tokens.AssertExpectations(t)
tlsCfg.AssertExpectations(t)
}
})
return &mockedConfig{
Config: Config{
DirectRPC: directRPC,
ServerProvider: serverProvider,
Cache: mcache,
Tokens: tokens,
TLSConfigurator: tlsCfg,
Logger: testutil.Logger(t),
},
directRPC: directRPC,
serverProvider: serverProvider,
cache: mcache,
tokens: tokens,
tlsCfg: tlsCfg,
}
}
func (m *mockedConfig) expectInitialTLS(t *testing.T, agentName, datacenter, token string, ca *structs.CARoot, indexedRoots *structs.IndexedCARoots, cert *structs.IssuedCert, extraCerts []string) {
var pems []string
for _, root := range indexedRoots.Roots {
pems = append(pems, root.RootCert)
}
// we should update the TLS configurator with the proper certs
m.tlsCfg.On("UpdateAutoTLS",
extraCerts,
pems,
cert.CertPEM,
// auto-config handles the CSR and Key so our tests don't have
// a way to know that the key is correct or not. We do replace
// a non empty PEM with "redacted" so we can ensure that some
// certificate is being sent
"redacted",
true,
).Return(nil).Once()
rootRes := cache.FetchResult{Value: indexedRoots, Index: indexedRoots.QueryMeta.Index}
rootsReq := structs.DCSpecificRequest{Datacenter: datacenter}
// we should prepopulate the cache with the CA roots
m.cache.On("Prepopulate",
cachetype.ConnectCARootName,
rootRes,
datacenter,
"",
rootsReq.CacheInfo().Key,
).Return(nil).Once()
leafReq := cachetype.ConnectCALeafRequest{
Token: token,
Agent: agentName,
Datacenter: datacenter,
}
// copy the cert and redact the private key for the mock expectation
// the actual private key will not correspond to the cert but thats
// because AutoConfig is generated a key/csr internally and sending that
// on up with the request.
copy := *cert
copy.PrivateKeyPEM = "redacted"
leafRes := cache.FetchResult{
Value: &copy,
Index: copy.RaftIndex.ModifyIndex,
State: cachetype.ConnectCALeafSuccess(ca.SigningKeyID),
}
// we should prepopulate the cache with the agents cert
m.cache.On("Prepopulate",
cachetype.ConnectCALeafName,
leafRes,
datacenter,
token,
leafReq.Key(),
).Return(nil).Once()
// when prepopulating the cert in the cache we grab the token so
// we should expec that here
m.tokens.On("AgentToken").Return(token).Once()
}
func (m *mockedConfig) setupInitialTLS(t *testing.T, agentName, datacenter, token string) (*structs.IndexedCARoots, *structs.IssuedCert, []string) {
ca, indexedRoots, cert := testCerts(t, agentName, datacenter)
ca2 := connect.TestCA(t, nil)
extraCerts := []string{ca2.RootCert}
m.expectInitialTLS(t, agentName, datacenter, token, ca, indexedRoots, cert, extraCerts)
return indexedRoots, cert, extraCerts
}

View File

@ -0,0 +1,86 @@
package autoconf
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/golang/protobuf/jsonpb"
"github.com/hashicorp/consul/proto/pbautoconf"
)
const (
// autoConfigFileName is the name of the file that the agent auto-config settings are
// stored in within the data directory
autoConfigFileName = "auto-config.json"
)
var (
pbMarshaler = &jsonpb.Marshaler{
OrigName: false,
EnumsAsInts: false,
Indent: " ",
EmitDefaults: true,
}
pbUnmarshaler = &jsonpb.Unmarshaler{
AllowUnknownFields: false,
}
)
func (ac *AutoConfig) readPersistedAutoConfig() (*pbautoconf.AutoConfigResponse, error) {
if ac.config.DataDir == "" {
// no data directory means we don't have anything to potentially load
return nil, nil
}
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
ac.logger.Debug("attempting to restore any persisted configuration", "path", path)
content, err := ioutil.ReadFile(path)
if err == nil {
rdr := strings.NewReader(string(content))
var resp pbautoconf.AutoConfigResponse
if err := pbUnmarshaler.Unmarshal(rdr, &resp); err != nil {
return nil, fmt.Errorf("failed to decode persisted auto-config data: %w", err)
}
ac.logger.Info("read persisted configuration", "path", path)
return &resp, nil
}
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to load %s: %w", path, err)
}
// ignore non-existence errors as that is an indicator that we haven't
// performed the auto configuration before
return nil, nil
}
func (ac *AutoConfig) persistAutoConfig(resp *pbautoconf.AutoConfigResponse) error {
// now that we know the configuration is generally fine including TLS certs go ahead and persist it to disk.
if ac.config.DataDir == "" {
ac.logger.Debug("not persisting auto-config settings because there is no data directory")
return nil
}
serialized, err := pbMarshaler.MarshalToString(resp)
if err != nil {
return fmt.Errorf("failed to encode auto-config response as JSON: %w", err)
}
path := filepath.Join(ac.config.DataDir, autoConfigFileName)
err = ioutil.WriteFile(path, []byte(serialized), 0660)
if err != nil {
return fmt.Errorf("failed to write auto-config configurations: %w", err)
}
ac.logger.Debug("auto-config settings were persisted to disk")
return nil
}

192
agent/auto-config/run.go Normal file
View File

@ -0,0 +1,192 @@
package autoconf
import (
"context"
"fmt"
"time"
"github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/structs"
)
// handleCacheEvent is used to handle event notifications from the cache for the roots
// or leaf cert watches.
func (ac *AutoConfig) handleCacheEvent(u cache.UpdateEvent) error {
switch u.CorrelationID {
case rootsWatchID:
ac.logger.Debug("roots watch fired - updating CA certificates")
if u.Err != nil {
return fmt.Errorf("root watch returned an error: %w", u.Err)
}
roots, ok := u.Result.(*structs.IndexedCARoots)
if !ok {
return fmt.Errorf("invalid type for roots watch response: %T", u.Result)
}
return ac.updateCARoots(roots)
case leafWatchID:
ac.logger.Debug("leaf certificate watch fired - updating TLS certificate")
if u.Err != nil {
return fmt.Errorf("leaf watch returned an error: %w", u.Err)
}
leaf, ok := u.Result.(*structs.IssuedCert)
if !ok {
return fmt.Errorf("invalid type for agent leaf cert watch response: %T", u.Result)
}
return ac.updateLeafCert(leaf)
}
return nil
}
// handleTokenUpdate is used when a notification about the agent token being updated
// is received and various watches need cancelling/restarting to use the new token.
func (ac *AutoConfig) handleTokenUpdate(ctx context.Context) error {
ac.logger.Debug("Agent token updated - resetting watches")
// TODO (autoencrypt) Prepopulate the cache with the new token with
// the existing cache entry with the old token. The certificate doesn't
// need to change just because the token has. However there isn't a
// good way to make that happen and this behavior is benign enough
// that I am going to push off implementing it.
// the agent token has been updated so we must update our leaf cert watch.
// this cancels the current watches before setting up new ones
ac.cancelWatches()
// recreate the chan for cache updates. This is a precautionary measure to ensure
// that we don't accidentally get notified for the new watches being setup before
// a blocking query in the cache returns and sends data to the old chan. In theory
// the code in agent/cache/watch.go should prevent this where we specifically check
// for context cancellation prior to sending the event. However we could cancel
// it after that check and finish setting up the new watches before getting the old
// events. Both the go routine scheduler and the OS thread scheduler would have to
// be acting up for this to happen. Regardless the way to ensure we don't get events
// for the old watches is to simply replace the chan we are expecting them from.
close(ac.cacheUpdates)
ac.cacheUpdates = make(chan cache.UpdateEvent, 10)
// restart watches - this will be done with the correct token
cancelWatches, err := ac.setupCertificateCacheWatches(ctx)
if err != nil {
return fmt.Errorf("failed to restart watches after agent token update: %w", err)
}
ac.cancelWatches = cancelWatches
return nil
}
// handleFallback is used when the current TLS certificate has expired and the normal
// updating mechanisms have failed to renew it quickly enough. This function will
// use the configured fallback mechanism to retrieve a new cert and start monitoring
// that one.
func (ac *AutoConfig) handleFallback(ctx context.Context) error {
ac.logger.Warn("agent's client certificate has expired")
// Background because the context is mainly useful when the agent is first starting up.
switch {
case ac.config.AutoConfig.Enabled:
resp, err := ac.getInitialConfiguration(ctx)
if err != nil {
return fmt.Errorf("error while retrieving new agent certificates via auto-config: %w", err)
}
return ac.recordInitialConfiguration(resp)
case ac.config.AutoEncryptTLS:
reply, err := ac.autoEncryptInitialCerts(ctx)
if err != nil {
return fmt.Errorf("error while retrieving new agent certificate via auto-encrypt: %w", err)
}
return ac.setInitialTLSCertificates(reply)
default:
return fmt.Errorf("logic error: either auto-encrypt or auto-config must be enabled")
}
}
// run is the private method to be spawn by the Start method for
// executing the main monitoring loop.
func (ac *AutoConfig) run(ctx context.Context, exit chan struct{}) {
// The fallbackTimer is used to notify AFTER the agents
// leaf certificate has expired and where we need
// to fall back to the less secure RPC endpoint just like
// if the agent was starting up new.
//
// Check 10sec (fallback leeway duration) after cert
// expires. The agent cache should be handling the expiration
// and renew it before then.
//
// If there is no cert, AutoEncryptCertNotAfter returns
// a value in the past which immediately triggers the
// renew, but this case shouldn't happen because at
// this point, auto_encrypt was just being setup
// successfully.
calcFallbackInterval := func() time.Duration {
certExpiry := ac.acConfig.TLSConfigurator.AutoEncryptCertNotAfter()
return certExpiry.Add(ac.acConfig.FallbackLeeway).Sub(time.Now())
}
fallbackTimer := time.NewTimer(calcFallbackInterval())
// cleanup for once we are stopped
defer func() {
// cancel the go routines performing the cache watches
ac.cancelWatches()
// ensure we don't leak the timers go routine
fallbackTimer.Stop()
// stop receiving notifications for token updates
ac.acConfig.Tokens.StopNotify(ac.tokenUpdates)
ac.logger.Debug("auto-config has been stopped")
ac.Lock()
ac.cancel = nil
ac.running = false
// this should be the final cleanup task as its what notifies
// the rest of the world that this go routine has exited.
close(exit)
ac.Unlock()
}()
for {
select {
case <-ctx.Done():
ac.logger.Debug("stopping auto-config")
return
case <-ac.tokenUpdates.Ch:
ac.logger.Debug("handling a token update event")
if err := ac.handleTokenUpdate(ctx); err != nil {
ac.logger.Error("error in handling token update event", "error", err)
}
case u := <-ac.cacheUpdates:
ac.logger.Debug("handling a cache update event", "correlation_id", u.CorrelationID)
if err := ac.handleCacheEvent(u); err != nil {
ac.logger.Error("error in handling cache update event", "error", err)
}
// reset the fallback timer as the certificate may have been updated
fallbackTimer.Stop()
fallbackTimer = time.NewTimer(calcFallbackInterval())
case <-fallbackTimer.C:
// This is a safety net in case the cert doesn't get renewed
// in time. The agent would be stuck in that case because the watches
// never use the AutoEncrypt.Sign endpoint.
// check auto encrypt client cert expiration
if ac.acConfig.TLSConfigurator.AutoEncryptCertExpired() {
if err := ac.handleFallback(ctx); err != nil {
ac.logger.Error("error when handling a certificate expiry event", "error", err)
fallbackTimer = time.NewTimer(ac.acConfig.FallbackRetry)
} else {
fallbackTimer = time.NewTimer(calcFallbackInterval())
}
} else {
// this shouldn't be possible. We calculate the timer duration to be the certificate
// expiration time + some leeway (10s default). So whenever we get here the certificate
// should be expired. Regardless its probably worth resetting the timer.
fallbackTimer = time.NewTimer(calcFallbackInterval())
}
}
}
}

View File

@ -0,0 +1,111 @@
package autoconf
import (
"fmt"
"net"
"strconv"
"strings"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-discover"
discoverk8s "github.com/hashicorp/go-discover/provider/k8s"
"github.com/hashicorp/go-hclog"
)
func (ac *AutoConfig) discoverServers(servers []string) ([]string, error) {
providers := make(map[string]discover.Provider)
for k, v := range discover.Providers {
providers[k] = v
}
providers["k8s"] = &discoverk8s.Provider{}
disco, err := discover.New(
discover.WithUserAgent(lib.UserAgent()),
discover.WithProviders(providers),
)
if err != nil {
return nil, fmt.Errorf("Failed to create go-discover resolver: %w", err)
}
var addrs []string
for _, addr := range servers {
switch {
case strings.Contains(addr, "provider="):
resolved, err := disco.Addrs(addr, ac.logger.StandardLogger(&hclog.StandardLoggerOptions{InferLevels: true}))
if err != nil {
ac.logger.Error("failed to resolve go-discover auto-config servers", "configuration", addr, "err", err)
continue
}
addrs = append(addrs, resolved...)
ac.logger.Debug("discovered auto-config servers", "servers", resolved)
default:
addrs = append(addrs, addr)
}
}
return addrs, nil
}
// autoConfigHosts is responsible for taking the list of server addresses
// and resolving any go-discover provider invocations. It will then return
// a list of hosts. These might be hostnames and is expected that DNS resolution
// may be performed after this function runs. Additionally these may contain
// ports so SplitHostPort could also be necessary.
func (ac *AutoConfig) autoConfigHosts() ([]string, error) {
// use servers known to gossip if there are any
if ac.acConfig.ServerProvider != nil {
if srv := ac.acConfig.ServerProvider.FindLANServer(); srv != nil {
return []string{srv.Addr.String()}, nil
}
}
addrs, err := ac.discoverServers(ac.config.AutoConfig.ServerAddresses)
if err != nil {
return nil, err
}
if len(addrs) == 0 {
return nil, fmt.Errorf("no auto-config server addresses available for use")
}
return addrs, nil
}
// resolveHost will take a single host string and convert it to a list of TCPAddrs
// This will process any port in the input as well as looking up the hostname using
// normal DNS resolution.
func (ac *AutoConfig) resolveHost(hostPort string) []net.TCPAddr {
port := ac.config.ServerPort
host, portStr, err := net.SplitHostPort(hostPort)
if err != nil {
if strings.Contains(err.Error(), "missing port in address") {
host = hostPort
} else {
ac.logger.Warn("error splitting host address into IP and port", "address", hostPort, "error", err)
return nil
}
} else {
port, err = strconv.Atoi(portStr)
if err != nil {
ac.logger.Warn("Parsed port is not an integer", "port", portStr, "error", err)
return nil
}
}
// resolve the host to a list of IPs
ips, err := net.LookupIP(host)
if err != nil {
ac.logger.Warn("IP resolution failed", "host", host, "error", err)
return nil
}
var addrs []net.TCPAddr
for _, ip := range ips {
addrs = append(addrs, net.TCPAddr{IP: ip, Port: port})
}
return addrs
}

280
agent/auto-config/tls.go Normal file
View File

@ -0,0 +1,280 @@
package autoconf
import (
"context"
"fmt"
"net"
"github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/proto/pbautoconf"
)
const (
// ID of the roots watch
rootsWatchID = "roots"
// ID of the leaf watch
leafWatchID = "leaf"
unknownTrustDomain = "unknown"
)
var (
defaultDNSSANs = []string{"localhost"}
defaultIPSANs = []net.IP{{127, 0, 0, 1}, net.ParseIP("::1")}
)
func extractPEMs(roots *structs.IndexedCARoots) []string {
var pems []string
for _, root := range roots.Roots {
pems = append(pems, root.RootCert)
}
return pems
}
// updateTLSFromResponse will update the TLS certificate and roots in the shared
// TLS configurator.
func (ac *AutoConfig) updateTLSFromResponse(resp *pbautoconf.AutoConfigResponse) error {
var pems []string
for _, root := range resp.GetCARoots().GetRoots() {
pems = append(pems, root.RootCert)
}
err := ac.acConfig.TLSConfigurator.UpdateAutoTLS(
resp.ExtraCACertificates,
pems,
resp.Certificate.GetCertPEM(),
resp.Certificate.GetPrivateKeyPEM(),
resp.Config.GetTLS().GetVerifyServerHostname(),
)
if err != nil {
return fmt.Errorf("Failed to update the TLS configurator with new certificates: %w", err)
}
return nil
}
func (ac *AutoConfig) setInitialTLSCertificates(certs *structs.SignedResponse) error {
if certs == nil {
return nil
}
if err := ac.populateCertificateCache(certs); err != nil {
return fmt.Errorf("error populating cache with certificates: %w", err)
}
connectCAPems := extractPEMs(&certs.ConnectCARoots)
err := ac.acConfig.TLSConfigurator.UpdateAutoTLS(
certs.ManualCARoots,
connectCAPems,
certs.IssuedCert.CertPEM,
certs.IssuedCert.PrivateKeyPEM,
certs.VerifyServerHostname,
)
if err != nil {
return fmt.Errorf("error updating TLS configurator with certificates: %w", err)
}
return nil
}
func (ac *AutoConfig) populateCertificateCache(certs *structs.SignedResponse) error {
cert, err := connect.ParseCert(certs.IssuedCert.CertPEM)
if err != nil {
return fmt.Errorf("Failed to parse certificate: %w", err)
}
// prepolutate roots cache
rootRes := cache.FetchResult{Value: &certs.ConnectCARoots, Index: certs.ConnectCARoots.QueryMeta.Index}
rootsReq := ac.caRootsRequest()
// getting the roots doesn't require a token so in order to potentially share the cache with another
if err := ac.acConfig.Cache.Prepopulate(cachetype.ConnectCARootName, rootRes, ac.config.Datacenter, "", rootsReq.CacheInfo().Key); err != nil {
return err
}
leafReq := ac.leafCertRequest()
// prepolutate leaf cache
certRes := cache.FetchResult{
Value: &certs.IssuedCert,
Index: certs.IssuedCert.RaftIndex.ModifyIndex,
State: cachetype.ConnectCALeafSuccess(connect.EncodeSigningKeyID(cert.AuthorityKeyId)),
}
if err := ac.acConfig.Cache.Prepopulate(cachetype.ConnectCALeafName, certRes, leafReq.Datacenter, leafReq.Token, leafReq.Key()); err != nil {
return err
}
return nil
}
func (ac *AutoConfig) setupCertificateCacheWatches(ctx context.Context) (context.CancelFunc, error) {
notificationCtx, cancel := context.WithCancel(ctx)
rootsReq := ac.caRootsRequest()
err := ac.acConfig.Cache.Notify(notificationCtx, cachetype.ConnectCARootName, &rootsReq, rootsWatchID, ac.cacheUpdates)
if err != nil {
cancel()
return nil, err
}
leafReq := ac.leafCertRequest()
err = ac.acConfig.Cache.Notify(notificationCtx, cachetype.ConnectCALeafName, &leafReq, leafWatchID, ac.cacheUpdates)
if err != nil {
cancel()
return nil, err
}
return cancel, nil
}
func (ac *AutoConfig) updateCARoots(roots *structs.IndexedCARoots) error {
switch {
case ac.config.AutoConfig.Enabled:
ac.Lock()
defer ac.Unlock()
var err error
ac.autoConfigResponse.CARoots, err = translateCARootsToProtobuf(roots)
if err != nil {
return err
}
if err := ac.updateTLSFromResponse(ac.autoConfigResponse); err != nil {
return err
}
return ac.persistAutoConfig(ac.autoConfigResponse)
case ac.config.AutoEncryptTLS:
pems := extractPEMs(roots)
if err := ac.acConfig.TLSConfigurator.UpdateAutoTLSCA(pems); err != nil {
return fmt.Errorf("failed to update Connect CA certificates: %w", err)
}
return nil
default:
return nil
}
}
func (ac *AutoConfig) updateLeafCert(cert *structs.IssuedCert) error {
switch {
case ac.config.AutoConfig.Enabled:
ac.Lock()
defer ac.Unlock()
var err error
ac.autoConfigResponse.Certificate, err = translateIssuedCertToProtobuf(cert)
if err != nil {
return err
}
if err := ac.updateTLSFromResponse(ac.autoConfigResponse); err != nil {
return err
}
return ac.persistAutoConfig(ac.autoConfigResponse)
case ac.config.AutoEncryptTLS:
if err := ac.acConfig.TLSConfigurator.UpdateAutoTLSCert(cert.CertPEM, cert.PrivateKeyPEM); err != nil {
return fmt.Errorf("failed to update the agent leaf cert: %w", err)
}
return nil
default:
return nil
}
}
func (ac *AutoConfig) caRootsRequest() structs.DCSpecificRequest {
return structs.DCSpecificRequest{Datacenter: ac.config.Datacenter}
}
func (ac *AutoConfig) leafCertRequest() cachetype.ConnectCALeafRequest {
return cachetype.ConnectCALeafRequest{
Datacenter: ac.config.Datacenter,
Agent: ac.config.NodeName,
DNSSAN: ac.getDNSSANs(),
IPSAN: ac.getIPSANs(),
Token: ac.acConfig.Tokens.AgentToken(),
}
}
// generateCSR will generate a CSR for an Agent certificate. This should
// be sent along with the AutoConfig.InitialConfiguration RPC or the
// AutoEncrypt.Sign RPC. The generated CSR does NOT have a real trust domain
// as when generating this we do not yet have the CA roots. The server will
// update the trust domain for us though.
func (ac *AutoConfig) generateCSR() (csr string, key string, err error) {
// We don't provide the correct host here, because we don't know any
// better at this point. Apart from the domain, we would need the
// ClusterID, which we don't have. This is why we go with
// unknownTrustDomain the first time. Subsequent CSRs will have the
// correct TrustDomain.
id := &connect.SpiffeIDAgent{
// will be replaced
Host: unknownTrustDomain,
Datacenter: ac.config.Datacenter,
Agent: ac.config.NodeName,
}
caConfig, err := ac.config.ConnectCAConfiguration()
if err != nil {
return "", "", fmt.Errorf("Cannot generate CSR: %w", err)
}
conf, err := caConfig.GetCommonConfig()
if err != nil {
return "", "", fmt.Errorf("Failed to load common CA configuration: %w", err)
}
if conf.PrivateKeyType == "" {
conf.PrivateKeyType = connect.DefaultPrivateKeyType
}
if conf.PrivateKeyBits == 0 {
conf.PrivateKeyBits = connect.DefaultPrivateKeyBits
}
// Create a new private key
pk, pkPEM, err := connect.GeneratePrivateKeyWithConfig(conf.PrivateKeyType, conf.PrivateKeyBits)
if err != nil {
return "", "", fmt.Errorf("Failed to generate private key: %w", err)
}
dnsNames := ac.getDNSSANs()
ipAddresses := ac.getIPSANs()
// Create a CSR.
//
// The Common Name includes the dummy trust domain for now but Server will
// override this when it is signed anyway so it's OK.
cn := connect.AgentCN(ac.config.NodeName, unknownTrustDomain)
csr, err = connect.CreateCSR(id, cn, pk, dnsNames, ipAddresses)
if err != nil {
return "", "", err
}
return csr, pkPEM, nil
}
func (ac *AutoConfig) getDNSSANs() []string {
sans := defaultDNSSANs
switch {
case ac.config.AutoConfig.Enabled:
sans = append(sans, ac.config.AutoConfig.DNSSANs...)
case ac.config.AutoEncryptTLS:
sans = append(sans, ac.config.AutoEncryptDNSSAN...)
}
return sans
}
func (ac *AutoConfig) getIPSANs() []net.IP {
sans := defaultIPSANs
switch {
case ac.config.AutoConfig.Enabled:
sans = append(sans, ac.config.AutoConfig.IPSANs...)
case ac.config.AutoEncryptTLS:
sans = append(sans, ac.config.AutoEncryptIPSAN...)
}
return sans
}

View File

@ -0,0 +1,56 @@
package autoconf
import (
"testing"
"time"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func newLeaf(t *testing.T, agentName, datacenter string, ca *structs.CARoot, idx uint64, expiration time.Duration) *structs.IssuedCert {
t.Helper()
pub, priv, err := connect.TestAgentLeaf(t, agentName, datacenter, ca, expiration)
require.NoError(t, err)
cert, err := connect.ParseCert(pub)
require.NoError(t, err)
spiffeID, err := connect.ParseCertURI(cert.URIs[0])
require.NoError(t, err)
agentID, ok := spiffeID.(*connect.SpiffeIDAgent)
require.True(t, ok, "certificate doesn't have an agent leaf cert URI")
return &structs.IssuedCert{
SerialNumber: cert.SerialNumber.String(),
CertPEM: pub,
PrivateKeyPEM: priv,
ValidAfter: cert.NotBefore,
ValidBefore: cert.NotAfter,
Agent: agentID.Agent,
AgentURI: agentID.URI().String(),
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
RaftIndex: structs.RaftIndex{
CreateIndex: idx,
ModifyIndex: idx,
},
}
}
func testCerts(t *testing.T, agentName, datacenter string) (*structs.CARoot, *structs.IndexedCARoots, *structs.IssuedCert) {
ca := connect.TestCA(t, nil)
ca.IntermediateCerts = make([]string, 0)
cert := newLeaf(t, agentName, datacenter, ca, 1, 10*time.Minute)
indexedRoots := structs.IndexedCARoots{
ActiveRootID: ca.ID,
TrustDomain: connect.TestClusterID,
Roots: []*structs.CARoot{
ca,
},
QueryMeta: structs.QueryMeta{Index: 1},
}
return ca, &indexedRoots, cert
}

38
agent/cache/cache.go vendored
View File

@ -144,16 +144,26 @@ type Options struct {
EntryFetchRate rate.Limit EntryFetchRate rate.Limit
} }
// New creates a new cache with the given RPC client and reasonable defaults. // Equal return true if both options are equivalent
// Further settings can be tweaked on the returned value. func (o Options) Equal(other Options) bool {
func New(options Options) *Cache { return o.EntryFetchMaxBurst == other.EntryFetchMaxBurst && o.EntryFetchRate == other.EntryFetchRate
}
// applyDefaultValuesOnOptions set default values on options and returned updated value
func applyDefaultValuesOnOptions(options Options) Options {
if options.EntryFetchRate == 0.0 { if options.EntryFetchRate == 0.0 {
options.EntryFetchRate = DefaultEntryFetchRate options.EntryFetchRate = DefaultEntryFetchRate
} }
if options.EntryFetchMaxBurst == 0 { if options.EntryFetchMaxBurst == 0 {
options.EntryFetchMaxBurst = DefaultEntryFetchMaxBurst options.EntryFetchMaxBurst = DefaultEntryFetchMaxBurst
} }
return options
}
// New creates a new cache with the given RPC client and reasonable defaults.
// Further settings can be tweaked on the returned value.
func New(options Options) *Cache {
options = applyDefaultValuesOnOptions(options)
// Initialize the heap. The buffer of 1 is really important because // Initialize the heap. The buffer of 1 is really important because
// its possible for the expiry loop to trigger the heap to update // its possible for the expiry loop to trigger the heap to update
// itself and it'd block forever otherwise. // itself and it'd block forever otherwise.
@ -234,6 +244,28 @@ func (c *Cache) RegisterType(n string, typ Type) {
c.types[n] = typeEntry{Name: n, Type: typ, Opts: &opts} c.types[n] = typeEntry{Name: n, Type: typ, Opts: &opts}
} }
// ReloadOptions updates the cache with the new options
// return true if Cache is updated, false if already up to date
func (c *Cache) ReloadOptions(options Options) bool {
options = applyDefaultValuesOnOptions(options)
modified := !options.Equal(c.options)
if modified {
c.entriesLock.RLock()
defer c.entriesLock.RUnlock()
for _, entry := range c.entries {
if c.options.EntryFetchRate != options.EntryFetchRate {
entry.FetchRateLimiter.SetLimit(options.EntryFetchRate)
}
if c.options.EntryFetchMaxBurst != options.EntryFetchMaxBurst {
entry.FetchRateLimiter.SetBurst(options.EntryFetchMaxBurst)
}
}
c.options.EntryFetchRate = options.EntryFetchRate
c.options.EntryFetchMaxBurst = options.EntryFetchMaxBurst
}
return modified
}
// Get loads the data for the given type and request. If data satisfying the // Get loads the data for the given type and request. If data satisfying the
// minimum index is present in the cache, it is returned immediately. Otherwise, // minimum index is present in the cache, it is returned immediately. Otherwise,
// this will block until the data is available or the request timeout is // this will block until the data is available or the request timeout is

View File

@ -14,6 +14,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"golang.org/x/time/rate"
) )
// Test a basic Get with no indexes (and therefore no blocking queries). // Test a basic Get with no indexes (and therefore no blocking queries).
@ -1220,6 +1221,64 @@ func TestCacheGet_nonBlockingType(t *testing.T) {
typ.AssertExpectations(t) typ.AssertExpectations(t)
} }
// Test a get with an index set will wait until an index that is higher
// is set in the cache.
func TestCacheReload(t *testing.T) {
t.Parallel()
typ1 := TestType(t)
defer typ1.AssertExpectations(t)
c := New(Options{EntryFetchRate: rate.Limit(1), EntryFetchMaxBurst: 1})
c.RegisterType("t1", typ1)
typ1.Mock.On("Fetch", mock.Anything, mock.Anything).Return(FetchResult{Value: 42, Index: 42}, nil).Maybe()
require.False(t, c.ReloadOptions(Options{EntryFetchRate: rate.Limit(1), EntryFetchMaxBurst: 1}), "Value should not be reloaded")
_, meta, err := c.Get(context.Background(), "t1", TestRequest(t, RequestInfo{Key: "hello1", MinIndex: uint64(1)}))
require.NoError(t, err)
require.Equal(t, meta.Index, uint64(42))
testEntry := func(t *testing.T, doTest func(t *testing.T, entry cacheEntry)) {
c.entriesLock.Lock()
tEntry, ok := c.types["t1"]
require.True(t, ok)
keyName := makeEntryKey("t1", "", "", "hello1")
ok, entryValid, entry := c.getEntryLocked(tEntry, keyName, RequestInfo{})
require.True(t, ok)
require.True(t, entryValid)
doTest(t, entry)
c.entriesLock.Unlock()
}
testEntry(t, func(t *testing.T, entry cacheEntry) {
require.Equal(t, entry.FetchRateLimiter.Limit(), rate.Limit(1))
require.Equal(t, entry.FetchRateLimiter.Burst(), 1)
})
// Modify only rateLimit
require.True(t, c.ReloadOptions(Options{EntryFetchRate: rate.Limit(100), EntryFetchMaxBurst: 1}))
testEntry(t, func(t *testing.T, entry cacheEntry) {
require.Equal(t, entry.FetchRateLimiter.Limit(), rate.Limit(100))
require.Equal(t, entry.FetchRateLimiter.Burst(), 1)
})
// Modify only Burst
require.True(t, c.ReloadOptions(Options{EntryFetchRate: rate.Limit(100), EntryFetchMaxBurst: 5}))
testEntry(t, func(t *testing.T, entry cacheEntry) {
require.Equal(t, entry.FetchRateLimiter.Limit(), rate.Limit(100))
require.Equal(t, entry.FetchRateLimiter.Burst(), 5)
})
// Modify only Burst and Limit at the same time
require.True(t, c.ReloadOptions(Options{EntryFetchRate: rate.Limit(1000), EntryFetchMaxBurst: 42}))
testEntry(t, func(t *testing.T, entry cacheEntry) {
require.Equal(t, entry.FetchRateLimiter.Limit(), rate.Limit(1000))
require.Equal(t, entry.FetchRateLimiter.Burst(), 42)
})
}
// TestCacheThrottle checks the assumptions for the cache throttling. It sets // TestCacheThrottle checks the assumptions for the cache throttling. It sets
// up a cache with Options{EntryFetchRate: 10.0, EntryFetchMaxBurst: 1}, which // up a cache with Options{EntryFetchRate: 10.0, EntryFetchMaxBurst: 1}, which
// allows for 10req/s, or one request every 100ms. // allows for 10req/s, or one request every 100ms.

View File

@ -60,7 +60,7 @@ func TestCacheNotifyChResult(t testing.T, ch <-chan UpdateEvent, expected ...Upd
} }
got := make([]UpdateEvent, 0, expectLen) got := make([]UpdateEvent, 0, expectLen)
timeoutCh := time.After(50 * time.Millisecond) timeoutCh := time.After(75 * time.Millisecond)
OUT: OUT:
for { for {
@ -74,7 +74,7 @@ OUT:
} }
case <-timeoutCh: case <-timeoutCh:
t.Fatalf("got %d results on chan in 50ms, want %d", len(got), expectLen) t.Fatalf("timeout while waiting for result: got %d results on chan, want %d", len(got), expectLen)
} }
} }

View File

@ -258,7 +258,7 @@ func TestCacheNotifyPolling(t *testing.T) {
} }
require.Equal(events[0].Result, 42) require.Equal(events[0].Result, 42)
require.Equal(events[0].Meta.Hit, false) require.Equal(events[0].Meta.Hit && events[1].Meta.Hit, false)
require.Equal(events[0].Meta.Index, uint64(1)) require.Equal(events[0].Meta.Index, uint64(1))
require.True(events[0].Meta.Age < 50*time.Millisecond) require.True(events[0].Meta.Age < 50*time.Millisecond)
require.NoError(events[0].Err) require.NoError(events[0].Err)

View File

@ -1,505 +0,0 @@
package certmon
import (
"context"
"fmt"
"io/ioutil"
"sync"
"time"
"github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/go-hclog"
)
const (
// ID of the roots watch
rootsWatchID = "roots"
// ID of the leaf watch
leafWatchID = "leaf"
)
// Cache is an interface to represent the methods of the
// agent/cache.Cache struct that we care about
type Cache interface {
Notify(ctx context.Context, t string, r cache.Request, correlationID string, ch chan<- cache.UpdateEvent) error
Prepopulate(t string, result cache.FetchResult, dc string, token string, key string) error
}
// CertMonitor will setup the proper watches to ensure that
// the Agent's Connect TLS certificate remains up to date
type CertMonitor struct {
logger hclog.Logger
cache Cache
tlsConfigurator *tlsutil.Configurator
tokens *token.Store
leafReq cachetype.ConnectCALeafRequest
rootsReq structs.DCSpecificRequest
persist PersistFunc
fallback FallbackFunc
fallbackLeeway time.Duration
fallbackRetry time.Duration
l sync.Mutex
running bool
// cancel is used to cancel the entire CertMonitor
// go routine. This is the main field protected
// by the mutex as it being non-nil indicates that
// the go routine has been started and is stoppable.
// note that it doesn't indcate that the go routine
// is currently running.
cancel context.CancelFunc
// cancelWatches is used to cancel the existing
// cache watches. This is mainly only necessary
// when the Agent token changes
cancelWatches context.CancelFunc
// cacheUpdates is the chan used to have the cache
// send us back events
cacheUpdates chan cache.UpdateEvent
// tokenUpdates is the struct used to receive
// events from the token store when the Agent
// token is updated.
tokenUpdates token.Notifier
// this is used to keep a local copy of the certs
// keys and ca certs. It will be used to persist
// all of the local state at once.
certs structs.SignedResponse
}
// New creates a new CertMonitor for automatically rotating
// an Agent's Connect Certificate
func New(config *Config) (*CertMonitor, error) {
logger := config.Logger
if logger == nil {
logger = hclog.New(&hclog.LoggerOptions{
Level: 0,
Output: ioutil.Discard,
})
}
if config.FallbackLeeway == 0 {
config.FallbackLeeway = 10 * time.Second
}
if config.FallbackRetry == 0 {
config.FallbackRetry = time.Minute
}
if config.Cache == nil {
return nil, fmt.Errorf("CertMonitor creation requires a Cache")
}
if config.TLSConfigurator == nil {
return nil, fmt.Errorf("CertMonitor creation requires a TLS Configurator")
}
if config.Fallback == nil {
return nil, fmt.Errorf("CertMonitor creation requires specifying a FallbackFunc")
}
if config.Datacenter == "" {
return nil, fmt.Errorf("CertMonitor creation requires specifying the datacenter")
}
if config.NodeName == "" {
return nil, fmt.Errorf("CertMonitor creation requires specifying the agent's node name")
}
if config.Tokens == nil {
return nil, fmt.Errorf("CertMonitor creation requires specifying a token store")
}
return &CertMonitor{
logger: logger,
cache: config.Cache,
tokens: config.Tokens,
tlsConfigurator: config.TLSConfigurator,
persist: config.Persist,
fallback: config.Fallback,
fallbackLeeway: config.FallbackLeeway,
fallbackRetry: config.FallbackRetry,
rootsReq: structs.DCSpecificRequest{Datacenter: config.Datacenter},
leafReq: cachetype.ConnectCALeafRequest{
Datacenter: config.Datacenter,
Agent: config.NodeName,
DNSSAN: config.DNSSANs,
IPSAN: config.IPSANs,
},
}, nil
}
// Update is responsible for priming the cache with the certificates
// as well as injecting them into the TLS configurator
func (m *CertMonitor) Update(certs *structs.SignedResponse) error {
if certs == nil {
return nil
}
m.certs = *certs
if err := m.populateCache(certs); err != nil {
return fmt.Errorf("error populating cache with certificates: %w", err)
}
connectCAPems := []string{}
for _, ca := range certs.ConnectCARoots.Roots {
connectCAPems = append(connectCAPems, ca.RootCert)
}
// Note that its expected that the private key be within the IssuedCert in the
// SignedResponse. This isn't how a server would send back the response and requires
// that the recipient of the response who also has access to the private key will
// have filled it in. The Cache definitely does this but auto-encrypt/auto-config
// will need to ensure the original response is setup this way too.
err := m.tlsConfigurator.UpdateAutoTLS(
certs.ManualCARoots,
connectCAPems,
certs.IssuedCert.CertPEM,
certs.IssuedCert.PrivateKeyPEM,
certs.VerifyServerHostname)
if err != nil {
return fmt.Errorf("error updating TLS configurator with certificates: %w", err)
}
return nil
}
// populateCache is responsible for inserting the certificates into the cache
func (m *CertMonitor) populateCache(resp *structs.SignedResponse) error {
cert, err := connect.ParseCert(resp.IssuedCert.CertPEM)
if err != nil {
return fmt.Errorf("Failed to parse certificate: %w", err)
}
// prepolutate roots cache
rootRes := cache.FetchResult{Value: &resp.ConnectCARoots, Index: resp.ConnectCARoots.QueryMeta.Index}
// getting the roots doesn't require a token so in order to potentially share the cache with another
if err := m.cache.Prepopulate(cachetype.ConnectCARootName, rootRes, m.rootsReq.Datacenter, "", m.rootsReq.CacheInfo().Key); err != nil {
return err
}
// copy the template and update the token
leafReq := m.leafReq
leafReq.Token = m.tokens.AgentToken()
// prepolutate leaf cache
certRes := cache.FetchResult{
Value: &resp.IssuedCert,
Index: resp.ConnectCARoots.QueryMeta.Index,
State: cachetype.ConnectCALeafSuccess(connect.EncodeSigningKeyID(cert.AuthorityKeyId)),
}
if err := m.cache.Prepopulate(cachetype.ConnectCALeafName, certRes, leafReq.Datacenter, leafReq.Token, leafReq.Key()); err != nil {
return err
}
return nil
}
// Start spawns the go routine to monitor the certificate and ensure it is
// rotated/renewed as necessary. The chan will indicate once the started
// go routine has exited
func (m *CertMonitor) Start(ctx context.Context) (<-chan struct{}, error) {
m.l.Lock()
defer m.l.Unlock()
if m.running || m.cancel != nil {
return nil, fmt.Errorf("the CertMonitor is already running")
}
// create the top level context to control the go
// routine executing the `run` method
ctx, cancel := context.WithCancel(ctx)
// create the channel to get cache update events through
// really we should only ever get 10 updates
m.cacheUpdates = make(chan cache.UpdateEvent, 10)
// setup the cache watches
cancelWatches, err := m.setupCacheWatches(ctx)
if err != nil {
cancel()
return nil, fmt.Errorf("error setting up cache watches: %w", err)
}
// start the token update notifier
m.tokenUpdates = m.tokens.Notify(token.TokenKindAgent)
// store the cancel funcs
m.cancel = cancel
m.cancelWatches = cancelWatches
m.running = true
exit := make(chan struct{})
go m.run(ctx, exit)
m.logger.Info("certificate monitor started")
return exit, nil
}
// Stop manually stops the go routine spawned by Start and
// returns whether the go routine was still running before
// cancelling.
//
// Note that cancelling the context passed into Start will
// also cause the go routine to stop
func (m *CertMonitor) Stop() bool {
m.l.Lock()
defer m.l.Unlock()
if !m.running {
return false
}
if m.cancel != nil {
m.cancel()
}
return true
}
// IsRunning returns whether the go routine to perform certificate monitoring
// is already running.
func (m *CertMonitor) IsRunning() bool {
m.l.Lock()
defer m.l.Unlock()
return m.running
}
// setupCacheWatches will start both the roots and leaf cert watch with a new child
// context and an up to date ACL token. The watches are started with a new child context
// whose CancelFunc is also returned.
func (m *CertMonitor) setupCacheWatches(ctx context.Context) (context.CancelFunc, error) {
notificationCtx, cancel := context.WithCancel(ctx)
// copy the request
rootsReq := m.rootsReq
err := m.cache.Notify(notificationCtx, cachetype.ConnectCARootName, &rootsReq, rootsWatchID, m.cacheUpdates)
if err != nil {
cancel()
return nil, err
}
// copy the request
leafReq := m.leafReq
leafReq.Token = m.tokens.AgentToken()
err = m.cache.Notify(notificationCtx, cachetype.ConnectCALeafName, &leafReq, leafWatchID, m.cacheUpdates)
if err != nil {
cancel()
return nil, err
}
return cancel, nil
}
// handleCacheEvent is used to handle event notifications from the cache for the roots
// or leaf cert watches.
func (m *CertMonitor) handleCacheEvent(u cache.UpdateEvent) error {
switch u.CorrelationID {
case rootsWatchID:
m.logger.Debug("roots watch fired - updating CA certificates")
if u.Err != nil {
return fmt.Errorf("root watch returned an error: %w", u.Err)
}
roots, ok := u.Result.(*structs.IndexedCARoots)
if !ok {
return fmt.Errorf("invalid type for roots watch response: %T", u.Result)
}
m.certs.ConnectCARoots = *roots
var pems []string
for _, root := range roots.Roots {
pems = append(pems, root.RootCert)
}
if err := m.tlsConfigurator.UpdateAutoTLSCA(pems); err != nil {
return fmt.Errorf("failed to update Connect CA certificates: %w", err)
}
if m.persist != nil {
copy := m.certs
if err := m.persist(&copy); err != nil {
return fmt.Errorf("failed to persist certificate package: %w", err)
}
}
case leafWatchID:
m.logger.Debug("leaf certificate watch fired - updating TLS certificate")
if u.Err != nil {
return fmt.Errorf("leaf watch returned an error: %w", u.Err)
}
leaf, ok := u.Result.(*structs.IssuedCert)
if !ok {
return fmt.Errorf("invalid type for agent leaf cert watch response: %T", u.Result)
}
m.certs.IssuedCert = *leaf
if err := m.tlsConfigurator.UpdateAutoTLSCert(leaf.CertPEM, leaf.PrivateKeyPEM); err != nil {
return fmt.Errorf("failed to update the agent leaf cert: %w", err)
}
if m.persist != nil {
copy := m.certs
if err := m.persist(&copy); err != nil {
return fmt.Errorf("failed to persist certificate package: %w", err)
}
}
}
return nil
}
// handleTokenUpdate is used when a notification about the agent token being updated
// is received and various watches need cancelling/restarting to use the new token.
func (m *CertMonitor) handleTokenUpdate(ctx context.Context) error {
m.logger.Debug("Agent token updated - resetting watches")
// TODO (autoencrypt) Prepopulate the cache with the new token with
// the existing cache entry with the old token. The certificate doesn't
// need to change just because the token has. However there isn't a
// good way to make that happen and this behavior is benign enough
// that I am going to push off implementing it.
// the agent token has been updated so we must update our leaf cert watch.
// this cancels the current watches before setting up new ones
m.cancelWatches()
// recreate the chan for cache updates. This is a precautionary measure to ensure
// that we don't accidentally get notified for the new watches being setup before
// a blocking query in the cache returns and sends data to the old chan. In theory
// the code in agent/cache/watch.go should prevent this where we specifically check
// for context cancellation prior to sending the event. However we could cancel
// it after that check and finish setting up the new watches before getting the old
// events. Both the go routine scheduler and the OS thread scheduler would have to
// be acting up for this to happen. Regardless the way to ensure we don't get events
// for the old watches is to simply replace the chan we are expecting them from.
close(m.cacheUpdates)
m.cacheUpdates = make(chan cache.UpdateEvent, 10)
// restart watches - this will be done with the correct token
cancelWatches, err := m.setupCacheWatches(ctx)
if err != nil {
return fmt.Errorf("failed to restart watches after agent token update: %w", err)
}
m.cancelWatches = cancelWatches
return nil
}
// handleFallback is used when the current TLS certificate has expired and the normal
// updating mechanisms have failed to renew it quickly enough. This function will
// use the configured fallback mechanism to retrieve a new cert and start monitoring
// that one.
func (m *CertMonitor) handleFallback(ctx context.Context) error {
m.logger.Warn("agent's client certificate has expired")
// Background because the context is mainly useful when the agent is first starting up.
reply, err := m.fallback(ctx)
if err != nil {
return fmt.Errorf("error when getting new agent certificate: %w", err)
}
if m.persist != nil {
if err := m.persist(reply); err != nil {
return fmt.Errorf("failed to persist certificate package: %w", err)
}
}
return m.Update(reply)
}
// run is the private method to be spawn by the Start method for
// executing the main monitoring loop.
func (m *CertMonitor) run(ctx context.Context, exit chan struct{}) {
// The fallbackTimer is used to notify AFTER the agents
// leaf certificate has expired and where we need
// to fall back to the less secure RPC endpoint just like
// if the agent was starting up new.
//
// Check 10sec (fallback leeway duration) after cert
// expires. The agent cache should be handling the expiration
// and renew it before then.
//
// If there is no cert, AutoEncryptCertNotAfter returns
// a value in the past which immediately triggers the
// renew, but this case shouldn't happen because at
// this point, auto_encrypt was just being setup
// successfully.
calcFallbackInterval := func() time.Duration {
certExpiry := m.tlsConfigurator.AutoEncryptCertNotAfter()
return certExpiry.Add(m.fallbackLeeway).Sub(time.Now())
}
fallbackTimer := time.NewTimer(calcFallbackInterval())
// cleanup for once we are stopped
defer func() {
// cancel the go routines performing the cache watches
m.cancelWatches()
// ensure we don't leak the timers go routine
fallbackTimer.Stop()
// stop receiving notifications for token updates
m.tokens.StopNotify(m.tokenUpdates)
m.logger.Debug("certificate monitor has been stopped")
m.l.Lock()
m.cancel = nil
m.running = false
m.l.Unlock()
// this should be the final cleanup task as its what notifies
// the rest of the world that this go routine has exited.
close(exit)
}()
for {
select {
case <-ctx.Done():
m.logger.Debug("stopping the certificate monitor")
return
case <-m.tokenUpdates.Ch:
m.logger.Debug("handling a token update event")
if err := m.handleTokenUpdate(ctx); err != nil {
m.logger.Error("error in handling token update event", "error", err)
}
case u := <-m.cacheUpdates:
m.logger.Debug("handling a cache update event", "correlation_id", u.CorrelationID)
if err := m.handleCacheEvent(u); err != nil {
m.logger.Error("error in handling cache update event", "error", err)
}
// reset the fallback timer as the certificate may have been updated
fallbackTimer.Stop()
fallbackTimer = time.NewTimer(calcFallbackInterval())
case <-fallbackTimer.C:
// This is a safety net in case the auto_encrypt cert doesn't get renewed
// in time. The agent would be stuck in that case because the watches
// never use the AutoEncrypt.Sign endpoint.
// check auto encrypt client cert expiration
if m.tlsConfigurator.AutoEncryptCertExpired() {
if err := m.handleFallback(ctx); err != nil {
m.logger.Error("error when handling a certificate expiry event", "error", err)
fallbackTimer = time.NewTimer(m.fallbackRetry)
} else {
fallbackTimer = time.NewTimer(calcFallbackInterval())
}
} else {
// this shouldn't be possible. We calculate the timer duration to be the certificate
// expiration time + some leeway (10s default). So whenever we get here the certificate
// should be expired. Regardless its probably worth resetting the timer.
fallbackTimer = time.NewTimer(calcFallbackInterval())
}
}
}
}

View File

@ -1,731 +0,0 @@
package certmon
import (
"context"
"crypto/tls"
"fmt"
"net"
"sync"
"testing"
"time"
"github.com/hashicorp/consul/agent/cache"
cachetype "github.com/hashicorp/consul/agent/cache-types"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/go-uuid"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockFallback struct {
mock.Mock
}
func (m *mockFallback) fallback(ctx context.Context) (*structs.SignedResponse, error) {
ret := m.Called()
resp, _ := ret.Get(0).(*structs.SignedResponse)
return resp, ret.Error(1)
}
type mockPersist struct {
mock.Mock
}
func (m *mockPersist) persist(resp *structs.SignedResponse) error {
return m.Called(resp).Error(0)
}
type mockWatcher struct {
ch chan<- cache.UpdateEvent
done <-chan struct{}
}
type mockCache struct {
mock.Mock
lock sync.Mutex
watchers map[string][]mockWatcher
}
func (m *mockCache) Notify(ctx context.Context, t string, r cache.Request, correlationID string, ch chan<- cache.UpdateEvent) error {
m.lock.Lock()
key := r.CacheInfo().Key
m.watchers[key] = append(m.watchers[key], mockWatcher{ch: ch, done: ctx.Done()})
m.lock.Unlock()
ret := m.Called(t, r, correlationID)
return ret.Error(0)
}
func (m *mockCache) Prepopulate(t string, result cache.FetchResult, dc string, token string, key string) error {
ret := m.Called(t, result, dc, token, key)
return ret.Error(0)
}
func (m *mockCache) sendNotification(ctx context.Context, key string, u cache.UpdateEvent) bool {
m.lock.Lock()
defer m.lock.Unlock()
watchers, ok := m.watchers[key]
if !ok || len(m.watchers) < 1 {
return false
}
var newWatchers []mockWatcher
for _, watcher := range watchers {
select {
case watcher.ch <- u:
newWatchers = append(newWatchers, watcher)
case <-watcher.done:
// do nothing, this watcher will be removed from the list
case <-ctx.Done():
// return doesn't matter here really, the test is being cancelled
return true
}
}
// this removes any already cancelled watches from being sent to
m.watchers[key] = newWatchers
return true
}
func newMockCache(t *testing.T) *mockCache {
mcache := mockCache{watchers: make(map[string][]mockWatcher)}
mcache.Test(t)
return &mcache
}
func waitForChan(timer *time.Timer, ch <-chan struct{}) bool {
select {
case <-timer.C:
return false
case <-ch:
return true
}
}
func waitForChans(timeout time.Duration, chans ...<-chan struct{}) bool {
timer := time.NewTimer(timeout)
defer timer.Stop()
for _, ch := range chans {
if !waitForChan(timer, ch) {
return false
}
}
return true
}
func testTLSConfigurator(t *testing.T) *tlsutil.Configurator {
t.Helper()
logger := testutil.Logger(t)
cfg, err := tlsutil.NewConfigurator(tlsutil.Config{AutoTLS: true}, logger)
require.NoError(t, err)
return cfg
}
func newLeaf(t *testing.T, ca *structs.CARoot, idx uint64, expiration time.Duration) *structs.IssuedCert {
t.Helper()
pub, priv, err := connect.TestAgentLeaf(t, "node", "foo", ca, expiration)
require.NoError(t, err)
cert, err := connect.ParseCert(pub)
require.NoError(t, err)
spiffeID, err := connect.ParseCertURI(cert.URIs[0])
require.NoError(t, err)
agentID, ok := spiffeID.(*connect.SpiffeIDAgent)
require.True(t, ok, "certificate doesn't have an agent leaf cert URI")
return &structs.IssuedCert{
SerialNumber: cert.SerialNumber.String(),
CertPEM: pub,
PrivateKeyPEM: priv,
ValidAfter: cert.NotBefore,
ValidBefore: cert.NotAfter,
Agent: agentID.Agent,
AgentURI: agentID.URI().String(),
EnterpriseMeta: *structs.DefaultEnterpriseMeta(),
RaftIndex: structs.RaftIndex{
CreateIndex: idx,
ModifyIndex: idx,
},
}
}
type testCertMonitor struct {
monitor *CertMonitor
mcache *mockCache
tls *tlsutil.Configurator
tokens *token.Store
fallback *mockFallback
persist *mockPersist
extraCACerts []string
initialCert *structs.IssuedCert
initialRoots *structs.IndexedCARoots
// these are some variables that the CertMonitor was created with
datacenter string
nodeName string
dns []string
ips []net.IP
verifyServerHostname bool
}
func newTestCertMonitor(t *testing.T) testCertMonitor {
t.Helper()
tlsConfigurator := testTLSConfigurator(t)
tokens := new(token.Store)
id, err := uuid.GenerateUUID()
require.NoError(t, err)
tokens.UpdateAgentToken(id, token.TokenSourceConfig)
ca := connect.TestCA(t, nil)
manualCA := connect.TestCA(t, nil)
// this cert is setup to not expire quickly. this will prevent
// the test from accidentally running the fallback routine
// before we want to force that to happen.
issued := newLeaf(t, ca, 1, 10*time.Minute)
indexedRoots := structs.IndexedCARoots{
ActiveRootID: ca.ID,
TrustDomain: connect.TestClusterID,
Roots: []*structs.CARoot{
ca,
},
QueryMeta: structs.QueryMeta{
Index: 1,
},
}
initialCerts := &structs.SignedResponse{
ConnectCARoots: indexedRoots,
IssuedCert: *issued,
ManualCARoots: []string{manualCA.RootCert},
VerifyServerHostname: true,
}
dnsSANs := []string{"test.dev"}
ipSANs := []net.IP{net.IPv4(198, 18, 0, 1)}
fallback := &mockFallback{}
fallback.Test(t)
persist := &mockPersist{}
persist.Test(t)
mcache := newMockCache(t)
rootRes := cache.FetchResult{Value: &indexedRoots, Index: 1}
rootsReq := structs.DCSpecificRequest{Datacenter: "foo"}
mcache.On("Prepopulate", cachetype.ConnectCARootName, rootRes, "foo", "", rootsReq.CacheInfo().Key).Return(nil).Once()
leafReq := cachetype.ConnectCALeafRequest{
Token: tokens.AgentToken(),
Agent: "node",
Datacenter: "foo",
DNSSAN: dnsSANs,
IPSAN: ipSANs,
}
leafRes := cache.FetchResult{
Value: issued,
Index: 1,
State: cachetype.ConnectCALeafSuccess(ca.SigningKeyID),
}
mcache.On("Prepopulate", cachetype.ConnectCALeafName, leafRes, "foo", tokens.AgentToken(), leafReq.Key()).Return(nil).Once()
// we can assert more later but this should always be done.
defer mcache.AssertExpectations(t)
cfg := new(Config).
WithCache(mcache).
WithLogger(testutil.Logger(t)).
WithTLSConfigurator(tlsConfigurator).
WithTokens(tokens).
WithFallback(fallback.fallback).
WithDNSSANs(dnsSANs).
WithIPSANs(ipSANs).
WithDatacenter("foo").
WithNodeName("node").
WithFallbackLeeway(time.Nanosecond).
WithFallbackRetry(time.Millisecond).
WithPersistence(persist.persist)
monitor, err := New(cfg)
require.NoError(t, err)
require.NotNil(t, monitor)
require.NoError(t, monitor.Update(initialCerts))
return testCertMonitor{
monitor: monitor,
tls: tlsConfigurator,
tokens: tokens,
mcache: mcache,
persist: persist,
fallback: fallback,
extraCACerts: []string{manualCA.RootCert},
initialCert: issued,
initialRoots: &indexedRoots,
datacenter: "foo",
nodeName: "node",
dns: dnsSANs,
ips: ipSANs,
verifyServerHostname: true,
}
}
func tlsCertificateFromIssued(t *testing.T, issued *structs.IssuedCert) *tls.Certificate {
t.Helper()
cert, err := tls.X509KeyPair([]byte(issued.CertPEM), []byte(issued.PrivateKeyPEM))
require.NoError(t, err)
return &cert
}
// convenience method to get a TLS Certificate from the intial issued certificate and priv key
func (cm *testCertMonitor) initialTLSCertificate(t *testing.T) *tls.Certificate {
t.Helper()
return tlsCertificateFromIssued(t, cm.initialCert)
}
// just a convenience method to get a list of all the CA pems that we set up regardless
// of manual vs connect.
func (cm *testCertMonitor) initialCACerts() []string {
pems := cm.extraCACerts
for _, root := range cm.initialRoots.Roots {
pems = append(pems, root.RootCert)
}
return pems
}
func (cm *testCertMonitor) assertExpectations(t *testing.T) {
cm.mcache.AssertExpectations(t)
cm.fallback.AssertExpectations(t)
cm.persist.AssertExpectations(t)
}
func TestCertMonitor_InitialCerts(t *testing.T) {
// this also ensures that the cache was prepopulated properly
cm := newTestCertMonitor(t)
// verify that the certificate was injected into the TLS configurator correctly
require.Equal(t, cm.initialTLSCertificate(t), cm.tls.Cert())
// verify that the CA certs (both Connect and manual ones) were injected correctly
require.ElementsMatch(t, cm.initialCACerts(), cm.tls.CAPems())
// verify that the auto-tls verify server hostname setting was injected correctly
require.Equal(t, cm.verifyServerHostname, cm.tls.VerifyServerHostname())
}
func TestCertMonitor_GoRoutineManagement(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
cm := newTestCertMonitor(t)
// ensure that the monitor is not running
require.False(t, cm.monitor.IsRunning())
// ensure that nothing bad happens and that it reports as stopped
require.False(t, cm.monitor.Stop())
// we will never send notifications so these just ignore everything
cm.mcache.On("Notify", cachetype.ConnectCARootName, &structs.DCSpecificRequest{Datacenter: cm.datacenter}, rootsWatchID).Return(nil).Times(2)
cm.mcache.On("Notify", cachetype.ConnectCALeafName,
&cachetype.ConnectCALeafRequest{
Token: cm.tokens.AgentToken(),
Datacenter: cm.datacenter,
Agent: cm.nodeName,
DNSSAN: cm.dns,
IPSAN: cm.ips,
},
leafWatchID,
).Return(nil).Times(2)
done, err := cm.monitor.Start(ctx)
require.NoError(t, err)
require.True(t, cm.monitor.IsRunning())
_, err = cm.monitor.Start(ctx)
testutil.RequireErrorContains(t, err, "the CertMonitor is already running")
require.True(t, cm.monitor.Stop())
require.True(t, waitForChans(100*time.Millisecond, done), "monitor didn't shut down")
require.False(t, cm.monitor.IsRunning())
done, err = cm.monitor.Start(ctx)
require.NoError(t, err)
// ensure that context cancellation causes us to stop as well
cancel()
require.True(t, waitForChans(100*time.Millisecond, done))
cm.assertExpectations(t)
}
func startedCertMonitor(t *testing.T) (context.Context, testCertMonitor) {
ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel)
cm := newTestCertMonitor(t)
rootsCtx, rootsCancel := context.WithCancel(ctx)
defer rootsCancel()
leafCtx, leafCancel := context.WithCancel(ctx)
defer leafCancel()
// initial roots watch
cm.mcache.On("Notify", cachetype.ConnectCARootName,
&structs.DCSpecificRequest{
Datacenter: cm.datacenter,
},
rootsWatchID).
Return(nil).
Once().
Run(func(_ mock.Arguments) {
rootsCancel()
})
// the initial watch after starting the monitor
cm.mcache.On("Notify", cachetype.ConnectCALeafName,
&cachetype.ConnectCALeafRequest{
Token: cm.tokens.AgentToken(),
Datacenter: cm.datacenter,
Agent: cm.nodeName,
DNSSAN: cm.dns,
IPSAN: cm.ips,
},
leafWatchID).
Return(nil).
Once().
Run(func(_ mock.Arguments) {
leafCancel()
})
done, err := cm.monitor.Start(ctx)
require.NoError(t, err)
// this prevents logs after the test finishes
t.Cleanup(func() {
cm.monitor.Stop()
<-done
})
require.True(t,
waitForChans(100*time.Millisecond, rootsCtx.Done(), leafCtx.Done()),
"not all watches were started within the alotted time")
return ctx, cm
}
// This test ensures that the cache watches are restarted with the updated
// token after receiving a token update
func TestCertMonitor_TokenUpdate(t *testing.T) {
ctx, cm := startedCertMonitor(t)
rootsCtx, rootsCancel := context.WithCancel(ctx)
defer rootsCancel()
leafCtx, leafCancel := context.WithCancel(ctx)
defer leafCancel()
newToken := "8e4fe8db-162d-42d8-81ca-710fb2280ad0"
// we expect a new roots watch because when the leaf cert watch is restarted so is the root cert watch
cm.mcache.On("Notify", cachetype.ConnectCARootName,
&structs.DCSpecificRequest{
Datacenter: cm.datacenter,
},
rootsWatchID).
Return(nil).
Once().
Run(func(_ mock.Arguments) {
rootsCancel()
})
secondWatch := &cachetype.ConnectCALeafRequest{
Token: newToken,
Datacenter: cm.datacenter,
Agent: cm.nodeName,
DNSSAN: cm.dns,
IPSAN: cm.ips,
}
// the new watch after updating the token
cm.mcache.On("Notify", cachetype.ConnectCALeafName, secondWatch, leafWatchID).
Return(nil).
Once().
Run(func(args mock.Arguments) {
leafCancel()
})
cm.tokens.UpdateAgentToken(newToken, token.TokenSourceAPI)
require.True(t,
waitForChans(100*time.Millisecond, rootsCtx.Done(), leafCtx.Done()),
"not all watches were restarted within the alotted time")
cm.assertExpectations(t)
}
func TestCertMonitor_RootsUpdate(t *testing.T) {
ctx, cm := startedCertMonitor(t)
secondCA := connect.TestCA(t, cm.initialRoots.Roots[0])
secondRoots := structs.IndexedCARoots{
ActiveRootID: secondCA.ID,
TrustDomain: connect.TestClusterID,
Roots: []*structs.CARoot{
secondCA,
cm.initialRoots.Roots[0],
},
QueryMeta: structs.QueryMeta{
Index: 99,
},
}
cm.persist.On("persist", &structs.SignedResponse{
IssuedCert: *cm.initialCert,
ManualCARoots: cm.extraCACerts,
ConnectCARoots: secondRoots,
VerifyServerHostname: cm.verifyServerHostname,
}).Return(nil).Once()
// assert value of the CA certs prior to updating
require.ElementsMatch(t, cm.initialCACerts(), cm.tls.CAPems())
req := structs.DCSpecificRequest{Datacenter: cm.datacenter}
require.True(t, cm.mcache.sendNotification(ctx, req.CacheInfo().Key, cache.UpdateEvent{
CorrelationID: rootsWatchID,
Result: &secondRoots,
Meta: cache.ResultMeta{
Index: secondRoots.Index,
},
}))
expectedCAs := append(cm.extraCACerts, secondCA.RootCert, cm.initialRoots.Roots[0].RootCert)
// this will wait up to 200ms (8 x 25 ms waits between the 9 requests)
retry.RunWith(&retry.Counter{Count: 9, Wait: 25 * time.Millisecond}, t, func(r *retry.R) {
require.ElementsMatch(r, expectedCAs, cm.tls.CAPems())
})
cm.assertExpectations(t)
}
func TestCertMonitor_CertUpdate(t *testing.T) {
ctx, cm := startedCertMonitor(t)
secondCert := newLeaf(t, cm.initialRoots.Roots[0], 100, 10*time.Minute)
cm.persist.On("persist", &structs.SignedResponse{
IssuedCert: *secondCert,
ManualCARoots: cm.extraCACerts,
ConnectCARoots: *cm.initialRoots,
VerifyServerHostname: cm.verifyServerHostname,
}).Return(nil).Once()
// assert value of cert prior to updating the leaf
require.Equal(t, cm.initialTLSCertificate(t), cm.tls.Cert())
key := cm.monitor.leafReq.CacheInfo().Key
// send the new certificate - this notifies only the watchers utilizing
// the new ACL token
require.True(t, cm.mcache.sendNotification(ctx, key, cache.UpdateEvent{
CorrelationID: leafWatchID,
Result: secondCert,
Meta: cache.ResultMeta{
Index: secondCert.ModifyIndex,
},
}))
tlsCert := tlsCertificateFromIssued(t, secondCert)
// this will wait up to 200ms (8 x 25 ms waits between the 9 requests)
retry.RunWith(&retry.Counter{Count: 9, Wait: 25 * time.Millisecond}, t, func(r *retry.R) {
require.Equal(r, tlsCert, cm.tls.Cert())
})
cm.assertExpectations(t)
}
func TestCertMonitor_Fallback(t *testing.T) {
ctx, cm := startedCertMonitor(t)
// at this point everything is operating normally and the monitor is just
// waiting for events. We are going to send a new cert that is basically
// already expired and then allow the fallback routine to kick in.
secondCert := newLeaf(t, cm.initialRoots.Roots[0], 100, time.Nanosecond)
secondCA := connect.TestCA(t, cm.initialRoots.Roots[0])
secondRoots := structs.IndexedCARoots{
ActiveRootID: secondCA.ID,
TrustDomain: connect.TestClusterID,
Roots: []*structs.CARoot{
secondCA,
cm.initialRoots.Roots[0],
},
QueryMeta: structs.QueryMeta{
Index: 101,
},
}
thirdCert := newLeaf(t, secondCA, 102, 10*time.Minute)
// inject a fallback routine error to check that we rerun it quickly
cm.fallback.On("fallback").Return(nil, fmt.Errorf("induced error")).Once()
fallbackResp := &structs.SignedResponse{
ConnectCARoots: secondRoots,
IssuedCert: *thirdCert,
ManualCARoots: cm.extraCACerts,
VerifyServerHostname: true,
}
// expect the fallback routine to be executed and setup the return
cm.fallback.On("fallback").Return(fallbackResp, nil).Once()
cm.persist.On("persist", &structs.SignedResponse{
IssuedCert: *secondCert,
ConnectCARoots: *cm.initialRoots,
ManualCARoots: cm.extraCACerts,
VerifyServerHostname: cm.verifyServerHostname,
}).Return(nil).Once()
cm.persist.On("persist", fallbackResp).Return(nil).Once()
// Add another roots cache prepopulation expectation which should happen
// in response to executing the fallback mechanism
rootRes := cache.FetchResult{Value: &secondRoots, Index: 101}
rootsReq := structs.DCSpecificRequest{Datacenter: cm.datacenter}
cm.mcache.On("Prepopulate", cachetype.ConnectCARootName, rootRes, cm.datacenter, "", rootsReq.CacheInfo().Key).Return(nil).Once()
// add another leaf cert cache prepopulation expectation which should happen
// in response to executing the fallback mechanism
leafReq := cachetype.ConnectCALeafRequest{
Token: cm.tokens.AgentToken(),
Agent: cm.nodeName,
Datacenter: cm.datacenter,
DNSSAN: cm.dns,
IPSAN: cm.ips,
}
leafRes := cache.FetchResult{
Value: thirdCert,
Index: 101,
State: cachetype.ConnectCALeafSuccess(secondCA.SigningKeyID),
}
cm.mcache.On("Prepopulate", cachetype.ConnectCALeafName, leafRes, leafReq.Datacenter, leafReq.Token, leafReq.Key()).Return(nil).Once()
// nothing in the monitor should be looking at this as its only done
// in response to sending token updates, no need to synchronize
key := cm.monitor.leafReq.CacheInfo().Key
// send the new certificate - this notifies only the watchers utilizing
// the new ACL token
require.True(t, cm.mcache.sendNotification(ctx, key, cache.UpdateEvent{
CorrelationID: leafWatchID,
Result: secondCert,
Meta: cache.ResultMeta{
Index: secondCert.ModifyIndex,
},
}))
// if all went well we would have updated the first certificate which was pretty much expired
// causing the fallback handler to be invoked almost immediately. The fallback routine will
// return the response containing the third cert and second CA roots so now we should wait
// a little while and ensure they were applied to the TLS Configurator
tlsCert := tlsCertificateFromIssued(t, thirdCert)
expectedCAs := append(cm.extraCACerts, secondCA.RootCert, cm.initialRoots.Roots[0].RootCert)
// this will wait up to 200ms (8 x 25 ms waits between the 9 requests)
retry.RunWith(&retry.Counter{Count: 9, Wait: 25 * time.Millisecond}, t, func(r *retry.R) {
require.Equal(r, tlsCert, cm.tls.Cert())
require.ElementsMatch(r, expectedCAs, cm.tls.CAPems())
})
cm.assertExpectations(t)
}
func TestCertMonitor_New_Errors(t *testing.T) {
type testCase struct {
cfg Config
err string
}
fallback := func(_ context.Context) (*structs.SignedResponse, error) {
return nil, fmt.Errorf("Unimplemented")
}
tokens := new(token.Store)
cases := map[string]testCase{
"no-cache": {
cfg: Config{
TLSConfigurator: testTLSConfigurator(t),
Fallback: fallback,
Tokens: tokens,
Datacenter: "foo",
NodeName: "bar",
},
err: "CertMonitor creation requires a Cache",
},
"no-tls-configurator": {
cfg: Config{
Cache: cache.New(cache.Options{}),
Fallback: fallback,
Tokens: tokens,
Datacenter: "foo",
NodeName: "bar",
},
err: "CertMonitor creation requires a TLS Configurator",
},
"no-fallback": {
cfg: Config{
Cache: cache.New(cache.Options{}),
TLSConfigurator: testTLSConfigurator(t),
Tokens: tokens,
Datacenter: "foo",
NodeName: "bar",
},
err: "CertMonitor creation requires specifying a FallbackFunc",
},
"no-tokens": {
cfg: Config{
Cache: cache.New(cache.Options{}),
TLSConfigurator: testTLSConfigurator(t),
Fallback: fallback,
Datacenter: "foo",
NodeName: "bar",
},
err: "CertMonitor creation requires specifying a token store",
},
"no-datacenter": {
cfg: Config{
Cache: cache.New(cache.Options{}),
TLSConfigurator: testTLSConfigurator(t),
Fallback: fallback,
Tokens: tokens,
NodeName: "bar",
},
err: "CertMonitor creation requires specifying the datacenter",
},
"no-node-name": {
cfg: Config{
Cache: cache.New(cache.Options{}),
TLSConfigurator: testTLSConfigurator(t),
Fallback: fallback,
Tokens: tokens,
Datacenter: "foo",
},
err: "CertMonitor creation requires specifying the agent's node name",
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
monitor, err := New(&tcase.cfg)
testutil.RequireErrorContains(t, err, tcase.err)
require.Nil(t, monitor)
})
}
}

View File

@ -1,150 +0,0 @@
package certmon
import (
"context"
"net"
"time"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/tlsutil"
"github.com/hashicorp/go-hclog"
)
// FallbackFunc is used when the normal cache watch based Certificate
// updating fails to update the Certificate in time and a different
// method of updating the certificate is required.
type FallbackFunc func(context.Context) (*structs.SignedResponse, error)
// PersistFunc is used to persist the data from a signed response
type PersistFunc func(*structs.SignedResponse) error
type Config struct {
// Logger is the logger to be used while running. If not set
// then no logging will be performed.
Logger hclog.Logger
// TLSConfigurator is where the certificates and roots are set when
// they are updated. This field is required.
TLSConfigurator *tlsutil.Configurator
// Cache is an object implementing our Cache interface. The Cache
// used at runtime must be able to handle Roots and Leaf Cert watches
Cache Cache
// Tokens is the shared token store. It is used to retrieve the current
// agent token as well as getting notifications when that token is updated.
// This field is required.
Tokens *token.Store
// Persist is a function to run when there are new certs or keys
Persist PersistFunc
// Fallback is a function to run when the normal cache updating of the
// agent's certificates has failed to work for one reason or another.
// This field is required.
Fallback FallbackFunc
// FallbackLeeway is the amount of time after certificate expiration before
// invoking the fallback routine. If not set this will default to 10s.
FallbackLeeway time.Duration
// FallbackRetry is the duration between Fallback invocations when the configured
// fallback routine returns an error. If not set this will default to 1m.
FallbackRetry time.Duration
// DNSSANs is a list of DNS SANs that certificate requests should include. This
// field is optional and no extra DNS SANs will be requested if unset. 'localhost'
// is unconditionally requested by the cache implementation.
DNSSANs []string
// IPSANs is a list of IP SANs to include in the certificate signing request. This
// field is optional and no extra IP SANs will be requested if unset. Both '127.0.0.1'
// and '::1' IP SANs are unconditionally requested by the cache implementation.
IPSANs []net.IP
// Datacenter is the datacenter to request certificates within. This filed is required
Datacenter string
// NodeName is the agent's node name to use when requesting certificates. This field
// is required.
NodeName string
}
// WithCache will cause the created CertMonitor type to use the provided Cache
func (cfg *Config) WithCache(cache Cache) *Config {
cfg.Cache = cache
return cfg
}
// WithLogger will cause the created CertMonitor type to use the provided logger
func (cfg *Config) WithLogger(logger hclog.Logger) *Config {
cfg.Logger = logger
return cfg
}
// WithTLSConfigurator will cause the created CertMonitor type to use the provided configurator
func (cfg *Config) WithTLSConfigurator(tlsConfigurator *tlsutil.Configurator) *Config {
cfg.TLSConfigurator = tlsConfigurator
return cfg
}
// WithTokens will cause the created CertMonitor type to use the provided token store
func (cfg *Config) WithTokens(tokens *token.Store) *Config {
cfg.Tokens = tokens
return cfg
}
// WithFallback configures a fallback function to use if the normal update mechanisms
// fail to renew the certificate in time.
func (cfg *Config) WithFallback(fallback FallbackFunc) *Config {
cfg.Fallback = fallback
return cfg
}
// WithDNSSANs configures the CertMonitor to request these DNS SANs when requesting a new
// certificate
func (cfg *Config) WithDNSSANs(sans []string) *Config {
cfg.DNSSANs = sans
return cfg
}
// WithIPSANs configures the CertMonitor to request these IP SANs when requesting a new
// certificate
func (cfg *Config) WithIPSANs(sans []net.IP) *Config {
cfg.IPSANs = sans
return cfg
}
// WithDatacenter configures the CertMonitor to request Certificates in this DC
func (cfg *Config) WithDatacenter(dc string) *Config {
cfg.Datacenter = dc
return cfg
}
// WithNodeName configures the CertMonitor to request Certificates with this agent name
func (cfg *Config) WithNodeName(name string) *Config {
cfg.NodeName = name
return cfg
}
// WithFallbackLeeway configures how long after a certificate expires before attempting to
// generarte a new certificate using the fallback mechanism. The default is 10s.
func (cfg *Config) WithFallbackLeeway(leeway time.Duration) *Config {
cfg.FallbackLeeway = leeway
return cfg
}
// WithFallbackRetry controls how quickly we will make subsequent invocations of
// the fallback func in the case of it erroring out.
func (cfg *Config) WithFallbackRetry(after time.Duration) *Config {
cfg.FallbackRetry = after
return cfg
}
// WithPersistence will configure the CertMonitor to use this callback for persisting
// a new TLS configuration.
func (cfg *Config) WithPersistence(persist PersistFunc) *Config {
cfg.Persist = persist
return cfg
}

View File

@ -22,6 +22,7 @@ import (
"github.com/hashicorp/consul/agent/consul/authmethod/ssoauth" "github.com/hashicorp/consul/agent/consul/authmethod/ssoauth"
"github.com/hashicorp/consul/agent/dns" "github.com/hashicorp/consul/agent/dns"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
libtempl "github.com/hashicorp/consul/lib/template" libtempl "github.com/hashicorp/consul/lib/template"
@ -799,6 +800,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
// build runtime config // build runtime config
// //
dataDir := b.stringVal(c.DataDir)
rt = RuntimeConfig{ rt = RuntimeConfig{
// non-user configurable values // non-user configurable values
ACLDisabledTTL: b.durationVal("acl.disabled_ttl", c.ACL.DisabledTTL), ACLDisabledTTL: b.durationVal("acl.disabled_ttl", c.ACL.DisabledTTL),
@ -837,21 +839,25 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
GossipWANRetransmitMult: b.intVal(c.GossipWAN.RetransmitMult), GossipWANRetransmitMult: b.intVal(c.GossipWAN.RetransmitMult),
// ACL // ACL
ACLsEnabled: aclsEnabled, ACLsEnabled: aclsEnabled,
ACLAgentMasterToken: b.stringValWithDefault(c.ACL.Tokens.AgentMaster, b.stringVal(c.ACLAgentMasterToken)), ACLDatacenter: primaryDatacenter,
ACLAgentToken: b.stringValWithDefault(c.ACL.Tokens.Agent, b.stringVal(c.ACLAgentToken)), ACLDefaultPolicy: b.stringValWithDefault(c.ACL.DefaultPolicy, b.stringVal(c.ACLDefaultPolicy)),
ACLDatacenter: primaryDatacenter, ACLDownPolicy: b.stringValWithDefault(c.ACL.DownPolicy, b.stringVal(c.ACLDownPolicy)),
ACLDefaultPolicy: b.stringValWithDefault(c.ACL.DefaultPolicy, b.stringVal(c.ACLDefaultPolicy)), ACLEnableKeyListPolicy: b.boolValWithDefault(c.ACL.EnableKeyListPolicy, b.boolVal(c.ACLEnableKeyListPolicy)),
ACLDownPolicy: b.stringValWithDefault(c.ACL.DownPolicy, b.stringVal(c.ACLDownPolicy)), ACLMasterToken: b.stringValWithDefault(c.ACL.Tokens.Master, b.stringVal(c.ACLMasterToken)),
ACLEnableKeyListPolicy: b.boolValWithDefault(c.ACL.EnableKeyListPolicy, b.boolVal(c.ACLEnableKeyListPolicy)), ACLTokenTTL: b.durationValWithDefault("acl.token_ttl", c.ACL.TokenTTL, b.durationVal("acl_ttl", c.ACLTTL)),
ACLMasterToken: b.stringValWithDefault(c.ACL.Tokens.Master, b.stringVal(c.ACLMasterToken)), ACLPolicyTTL: b.durationVal("acl.policy_ttl", c.ACL.PolicyTTL),
ACLReplicationToken: b.stringValWithDefault(c.ACL.Tokens.Replication, b.stringVal(c.ACLReplicationToken)), ACLRoleTTL: b.durationVal("acl.role_ttl", c.ACL.RoleTTL),
ACLTokenTTL: b.durationValWithDefault("acl.token_ttl", c.ACL.TokenTTL, b.durationVal("acl_ttl", c.ACLTTL)), ACLTokenReplication: b.boolValWithDefault(c.ACL.TokenReplication, b.boolValWithDefault(c.EnableACLReplication, enableTokenReplication)),
ACLPolicyTTL: b.durationVal("acl.policy_ttl", c.ACL.PolicyTTL),
ACLRoleTTL: b.durationVal("acl.role_ttl", c.ACL.RoleTTL), ACLTokens: token.Config{
ACLToken: b.stringValWithDefault(c.ACL.Tokens.Default, b.stringVal(c.ACLToken)), DataDir: dataDir,
ACLTokenReplication: b.boolValWithDefault(c.ACL.TokenReplication, b.boolValWithDefault(c.EnableACLReplication, enableTokenReplication)), EnablePersistence: b.boolValWithDefault(c.ACL.EnableTokenPersistence, false),
ACLEnableTokenPersistence: b.boolValWithDefault(c.ACL.EnableTokenPersistence, false), ACLDefaultToken: b.stringValWithDefault(c.ACL.Tokens.Default, b.stringVal(c.ACLToken)),
ACLAgentToken: b.stringValWithDefault(c.ACL.Tokens.Agent, b.stringVal(c.ACLAgentToken)),
ACLAgentMasterToken: b.stringValWithDefault(c.ACL.Tokens.AgentMaster, b.stringVal(c.ACLAgentMasterToken)),
ACLReplicationToken: b.stringValWithDefault(c.ACL.Tokens.Replication, b.stringVal(c.ACLReplicationToken)),
},
// Autopilot // Autopilot
AutopilotCleanupDeadServers: b.boolVal(c.Autopilot.CleanupDeadServers), AutopilotCleanupDeadServers: b.boolVal(c.Autopilot.CleanupDeadServers),
@ -957,7 +963,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
ConnectTestCALeafRootChangeSpread: b.durationVal("connect.test_ca_leaf_root_change_spread", c.Connect.TestCALeafRootChangeSpread), ConnectTestCALeafRootChangeSpread: b.durationVal("connect.test_ca_leaf_root_change_spread", c.Connect.TestCALeafRootChangeSpread),
ExposeMinPort: exposeMinPort, ExposeMinPort: exposeMinPort,
ExposeMaxPort: exposeMaxPort, ExposeMaxPort: exposeMaxPort,
DataDir: b.stringVal(c.DataDir), DataDir: dataDir,
Datacenter: datacenter, Datacenter: datacenter,
DefaultQueryTime: b.durationVal("default_query_time", c.DefaultQueryTime), DefaultQueryTime: b.durationVal("default_query_time", c.DefaultQueryTime),
DevMode: b.boolVal(b.devMode), DevMode: b.boolVal(b.devMode),
@ -1072,10 +1078,8 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
return RuntimeConfig{}, fmt.Errorf("cache.entry_fetch_rate must be strictly positive, was: %v", rt.Cache.EntryFetchRate) return RuntimeConfig{}, fmt.Errorf("cache.entry_fetch_rate must be strictly positive, was: %v", rt.Cache.EntryFetchRate)
} }
if entCfg, err := b.BuildEnterpriseRuntimeConfig(&c); err != nil { if err := b.BuildEnterpriseRuntimeConfig(&rt, &c); err != nil {
return RuntimeConfig{}, err return rt, err
} else {
rt.EnterpriseRuntimeConfig = entCfg
} }
if rt.BootstrapExpect == 1 { if rt.BootstrapExpect == 1 {
@ -1363,7 +1367,8 @@ func (b *Builder) Validate(rt RuntimeConfig) error {
b.warn(err.Error()) b.warn(err.Error())
} }
return nil err := b.validateEnterpriseConfig(rt)
return err
} }
// addrUnique checks if the given address is already in use for another // addrUnique checks if the given address is already in use for another

View File

@ -51,8 +51,12 @@ func (e enterpriseConfigKeyError) Error() string {
return fmt.Sprintf("%q is a Consul Enterprise configuration and will have no effect", e.key) return fmt.Sprintf("%q is a Consul Enterprise configuration and will have no effect", e.key)
} }
func (_ *Builder) BuildEnterpriseRuntimeConfig(_ *Config) (EnterpriseRuntimeConfig, error) { func (*Builder) BuildEnterpriseRuntimeConfig(_ *RuntimeConfig, _ *Config) error {
return EnterpriseRuntimeConfig{}, nil return nil
}
func (*Builder) validateEnterpriseConfig(_ RuntimeConfig) error {
return nil
} }
// validateEnterpriseConfig is a function to validate the enterprise specific // validateEnterpriseConfig is a function to validate the enterprise specific

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/api" "github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/logging"
@ -63,19 +64,7 @@ type RuntimeConfig struct {
// hcl: acl.enabled = boolean // hcl: acl.enabled = boolean
ACLsEnabled bool ACLsEnabled bool
// ACLAgentMasterToken is a special token that has full read and write ACLTokens token.Config
// privileges for this agent, and can be used to call agent endpoints
// when no servers are available.
//
// hcl: acl.tokens.agent_master = string
ACLAgentMasterToken string
// ACLAgentToken is the default token used to make requests for the agent
// itself, such as for registering itself with the catalog. If not
// configured, the 'acl_token' will be used.
//
// hcl: acl.tokens.agent = string
ACLAgentToken string
// ACLDatacenter is the central datacenter that holds authoritative // ACLDatacenter is the central datacenter that holds authoritative
// ACL records. This must be the same for the entire cluster. // ACL records. This must be the same for the entire cluster.
@ -123,16 +112,6 @@ type RuntimeConfig struct {
// hcl: acl.tokens.master = string // hcl: acl.tokens.master = string
ACLMasterToken string ACLMasterToken string
// ACLReplicationToken is used to replicate data locally from the
// PrimaryDatacenter. Replication is only available on servers in
// datacenters other than the PrimaryDatacenter
//
// DEPRECATED (ACL-Legacy-Compat): Setting this to a non-empty value
// also enables legacy ACL replication if ACLs are enabled and in legacy mode.
//
// hcl: acl.tokens.replication = string
ACLReplicationToken string
// ACLtokenReplication is used to indicate that both tokens and policies // ACLtokenReplication is used to indicate that both tokens and policies
// should be replicated instead of just policies // should be replicated instead of just policies
// //
@ -157,16 +136,6 @@ type RuntimeConfig struct {
// hcl: acl.role_ttl = "duration" // hcl: acl.role_ttl = "duration"
ACLRoleTTL time.Duration ACLRoleTTL time.Duration
// ACLToken is the default token used to make requests if a per-request
// token is not provided. If not configured the 'anonymous' token is used.
//
// hcl: acl.tokens.default = string
ACLToken string
// ACLEnableTokenPersistence determines whether or not tokens set via the agent HTTP API
// should be persisted to disk and reloaded when an agent restarts.
ACLEnableTokenPersistence bool
// AutopilotCleanupDeadServers enables the automatic cleanup of dead servers when new ones // AutopilotCleanupDeadServers enables the automatic cleanup of dead servers when new ones
// are added to the peer list. Defaults to true. // are added to the peer list. Defaults to true.
// //

View File

@ -6,11 +6,9 @@ var entMetaJSON = `{}`
var entRuntimeConfigSanitize = `{}` var entRuntimeConfigSanitize = `{}`
var entFullDNSJSONConfig = `` var entTokenConfigSanitize = `"EnterpriseConfig": {},`
var entFullDNSHCLConfig = `` func entFullRuntimeConfig(rt *RuntimeConfig) {}
var entFullRuntimeConfig = EnterpriseRuntimeConfig{}
var enterpriseNonVotingServerWarnings []string = []string{enterpriseConfigKeyError{key: "non_voting_server"}.Error()} var enterpriseNonVotingServerWarnings []string = []string{enterpriseConfigKeyError{key: "non_voting_server"}.Error()}

View File

@ -21,6 +21,7 @@ import (
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
"github.com/hashicorp/consul/agent/checks" "github.com/hashicorp/consul/agent/checks"
"github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/logging" "github.com/hashicorp/consul/logging"
"github.com/hashicorp/consul/sdk/testutil" "github.com/hashicorp/consul/sdk/testutil"
@ -1613,7 +1614,7 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
json: []string{`{ "acl_replication_token": "a" }`}, json: []string{`{ "acl_replication_token": "a" }`},
hcl: []string{`acl_replication_token = "a"`}, hcl: []string{`acl_replication_token = "a"`},
patch: func(rt *RuntimeConfig) { patch: func(rt *RuntimeConfig) {
rt.ACLReplicationToken = "a" rt.ACLTokens.ACLReplicationToken = "a"
rt.ACLTokenReplication = true rt.ACLTokenReplication = true
rt.DataDir = dataDir rt.DataDir = dataDir
}, },
@ -3436,6 +3437,10 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
{ {
"kind": "service-defaults", "kind": "service-defaults",
"name": "web", "name": "web",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"protocol": "http", "protocol": "http",
"external_sni": "abc-123", "external_sni": "abc-123",
"mesh_gateway": { "mesh_gateway": {
@ -3450,6 +3455,10 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
bootstrap { bootstrap {
kind = "service-defaults" kind = "service-defaults"
name = "web" name = "web"
meta {
"foo" = "bar"
"gir" = "zim"
}
protocol = "http" protocol = "http"
external_sni = "abc-123" external_sni = "abc-123"
mesh_gateway { mesh_gateway {
@ -3461,8 +3470,12 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
rt.DataDir = dataDir rt.DataDir = dataDir
rt.ConfigEntryBootstrap = []structs.ConfigEntry{ rt.ConfigEntryBootstrap = []structs.ConfigEntry{
&structs.ServiceConfigEntry{ &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults, Kind: structs.ServiceDefaults,
Name: "web", Name: "web",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
EnterpriseMeta: *defaultEntMeta, EnterpriseMeta: *defaultEntMeta,
Protocol: "http", Protocol: "http",
ExternalSNI: "abc-123", ExternalSNI: "abc-123",
@ -3482,6 +3495,10 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
{ {
"Kind": "service-defaults", "Kind": "service-defaults",
"Name": "web", "Name": "web",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Protocol": "http", "Protocol": "http",
"ExternalSNI": "abc-123", "ExternalSNI": "abc-123",
"MeshGateway": { "MeshGateway": {
@ -3496,6 +3513,10 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
bootstrap { bootstrap {
Kind = "service-defaults" Kind = "service-defaults"
Name = "web" Name = "web"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Protocol = "http" Protocol = "http"
ExternalSNI = "abc-123" ExternalSNI = "abc-123"
MeshGateway { MeshGateway {
@ -3507,8 +3528,12 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
rt.DataDir = dataDir rt.DataDir = dataDir
rt.ConfigEntryBootstrap = []structs.ConfigEntry{ rt.ConfigEntryBootstrap = []structs.ConfigEntry{
&structs.ServiceConfigEntry{ &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults, Kind: structs.ServiceDefaults,
Name: "web", Name: "web",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
EnterpriseMeta: *defaultEntMeta, EnterpriseMeta: *defaultEntMeta,
Protocol: "http", Protocol: "http",
ExternalSNI: "abc-123", ExternalSNI: "abc-123",
@ -3528,6 +3553,10 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
{ {
"kind": "service-router", "kind": "service-router",
"name": "main", "name": "main",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"routes": [ "routes": [
{ {
"match": { "match": {
@ -3612,6 +3641,10 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
bootstrap { bootstrap {
kind = "service-router" kind = "service-router"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
routes = [ routes = [
{ {
match { match {
@ -3693,8 +3726,12 @@ func TestBuilder_BuildAndValide_ConfigFlagsAndEdgecases(t *testing.T) {
rt.DataDir = dataDir rt.DataDir = dataDir
rt.ConfigEntryBootstrap = []structs.ConfigEntry{ rt.ConfigEntryBootstrap = []structs.ConfigEntry{
&structs.ServiceRouterConfigEntry{ &structs.ServiceRouterConfigEntry{
Kind: structs.ServiceRouter, Kind: structs.ServiceRouter,
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
EnterpriseMeta: *defaultEntMeta, EnterpriseMeta: *defaultEntMeta,
Routes: []structs.ServiceRoute{ Routes: []structs.ServiceRoute{
{ {
@ -4350,6 +4387,13 @@ func testConfig(t *testing.T, tests []configTest, dataDir string) {
if tt.patch != nil { if tt.patch != nil {
tt.patch(&expected) tt.patch(&expected)
} }
// both DataDir fields should always be the same, so test for the
// invariant, and than updated the expected, so that every test
// case does not need to set this field.
require.Equal(t, actual.DataDir, actual.ACLTokens.DataDir)
expected.ACLTokens.DataDir = actual.ACLTokens.DataDir
require.Equal(t, expected, actual) require.Equal(t, expected, actual)
}) })
} }
@ -5843,20 +5887,24 @@ func TestFullConfig(t *testing.T) {
// user configurable values // user configurable values
ACLAgentMasterToken: "64fd0e08", ACLTokens: token.Config{
ACLAgentToken: "bed2377c", EnablePersistence: true,
DataDir: dataDir,
ACLDefaultToken: "418fdff1",
ACLAgentToken: "bed2377c",
ACLAgentMasterToken: "64fd0e08",
ACLReplicationToken: "5795983a",
},
ACLsEnabled: true, ACLsEnabled: true,
ACLDatacenter: "ejtmd43d", ACLDatacenter: "ejtmd43d",
ACLDefaultPolicy: "72c2e7a0", ACLDefaultPolicy: "72c2e7a0",
ACLDownPolicy: "03eb2aee", ACLDownPolicy: "03eb2aee",
ACLEnableKeyListPolicy: true, ACLEnableKeyListPolicy: true,
ACLEnableTokenPersistence: true,
ACLMasterToken: "8a19ac27", ACLMasterToken: "8a19ac27",
ACLReplicationToken: "5795983a",
ACLTokenTTL: 3321 * time.Second, ACLTokenTTL: 3321 * time.Second,
ACLPolicyTTL: 1123 * time.Second, ACLPolicyTTL: 1123 * time.Second,
ACLRoleTTL: 9876 * time.Second, ACLRoleTTL: 9876 * time.Second,
ACLToken: "418fdff1",
ACLTokenReplication: true, ACLTokenReplication: true,
AdvertiseAddrLAN: ipAddr("17.99.29.16"), AdvertiseAddrLAN: ipAddr("17.99.29.16"),
AdvertiseAddrWAN: ipAddr("78.63.37.19"), AdvertiseAddrWAN: ipAddr("78.63.37.19"),
@ -6485,9 +6533,10 @@ func TestFullConfig(t *testing.T) {
"args": []interface{}{"dltjDJ2a", "flEa7C2d"}, "args": []interface{}{"dltjDJ2a", "flEa7C2d"},
}, },
}, },
EnterpriseRuntimeConfig: entFullRuntimeConfig,
} }
entFullRuntimeConfig(&want)
warns := []string{ warns := []string{
`The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`, `The 'acl_datacenter' field is deprecated. Use the 'primary_datacenter' field instead.`,
`bootstrap_expect > 0: expecting 53 servers`, `bootstrap_expect > 0: expecting 53 servers`,
@ -6804,21 +6853,25 @@ func TestSanitize(t *testing.T) {
} }
rtJSON := `{ rtJSON := `{
"ACLAgentMasterToken": "hidden", "ACLTokens": {
"ACLAgentToken": "hidden", ` + entTokenConfigSanitize + `
"ACLAgentMasterToken": "hidden",
"ACLAgentToken": "hidden",
"ACLDefaultToken": "hidden",
"ACLReplicationToken": "hidden",
"DataDir": "",
"EnablePersistence": false
},
"ACLDatacenter": "", "ACLDatacenter": "",
"ACLDefaultPolicy": "", "ACLDefaultPolicy": "",
"ACLDisabledTTL": "0s", "ACLDisabledTTL": "0s",
"ACLDownPolicy": "", "ACLDownPolicy": "",
"ACLEnableKeyListPolicy": false, "ACLEnableKeyListPolicy": false,
"ACLEnableTokenPersistence": false,
"ACLMasterToken": "hidden", "ACLMasterToken": "hidden",
"ACLPolicyTTL": "0s", "ACLPolicyTTL": "0s",
"ACLReplicationToken": "hidden",
"ACLRoleTTL": "0s", "ACLRoleTTL": "0s",
"ACLTokenReplication": false, "ACLTokenReplication": false,
"ACLTokenTTL": "0s", "ACLTokenTTL": "0s",
"ACLToken": "hidden",
"ACLsEnabled": false, "ACLsEnabled": false,
"AEInterval": "0s", "AEInterval": "0s",
"AdvertiseAddrLAN": "", "AdvertiseAddrLAN": "",

View File

@ -1639,8 +1639,8 @@ func TestACLResolver_Client(t *testing.T) {
// effectively disable caching - so the only way we end up with 1 token read is if they were // effectively disable caching - so the only way we end up with 1 token read is if they were
// being resolved concurrently // being resolved concurrently
config.Config.ACLTokenTTL = 0 * time.Second config.Config.ACLTokenTTL = 0 * time.Second
config.Config.ACLPolicyTTL = 30 * time.Millisecond config.Config.ACLPolicyTTL = 30 * time.Second
config.Config.ACLRoleTTL = 30 * time.Millisecond config.Config.ACLRoleTTL = 30 * time.Second
config.Config.ACLDownPolicy = "extend-cache" config.Config.ACLDownPolicy = "extend-cache"
}) })

View File

@ -1,239 +0,0 @@
package consul
import (
"context"
"fmt"
"net"
"strings"
"time"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-hclog"
"github.com/miekg/dns"
)
const (
dummyTrustDomain = "dummy.trustdomain"
retryJitterWindow = 30 * time.Second
)
func (c *Client) autoEncryptCSR(extraDNSSANs []string, extraIPSANs []net.IP) (string, string, error) {
// We don't provide the correct host here, because we don't know any
// better at this point. Apart from the domain, we would need the
// ClusterID, which we don't have. This is why we go with
// dummyTrustDomain the first time. Subsequent CSRs will have the
// correct TrustDomain.
id := &connect.SpiffeIDAgent{
Host: dummyTrustDomain,
Datacenter: c.config.Datacenter,
Agent: c.config.NodeName,
}
conf, err := c.config.CAConfig.GetCommonConfig()
if err != nil {
return "", "", err
}
if conf.PrivateKeyType == "" {
conf.PrivateKeyType = connect.DefaultPrivateKeyType
}
if conf.PrivateKeyBits == 0 {
conf.PrivateKeyBits = connect.DefaultPrivateKeyBits
}
// Create a new private key
pk, pkPEM, err := connect.GeneratePrivateKeyWithConfig(conf.PrivateKeyType, conf.PrivateKeyBits)
if err != nil {
return "", "", err
}
dnsNames := append([]string{"localhost"}, extraDNSSANs...)
ipAddresses := append([]net.IP{net.ParseIP("127.0.0.1"), net.ParseIP("::1")}, extraIPSANs...)
// Create a CSR.
//
// The Common Name includes the dummy trust domain for now but Server will
// override this when it is signed anyway so it's OK.
cn := connect.AgentCN(c.config.NodeName, dummyTrustDomain)
csr, err := connect.CreateCSR(id, cn, pk, dnsNames, ipAddresses)
if err != nil {
return "", "", err
}
return pkPEM, csr, nil
}
func (c *Client) RequestAutoEncryptCerts(ctx context.Context, servers []string, port int, token string, extraDNSSANs []string, extraIPSANs []net.IP) (*structs.SignedResponse, error) {
errFn := func(err error) (*structs.SignedResponse, error) {
return nil, err
}
// Check if we know about a server already through gossip. Depending on
// how the agent joined, there might already be one. Also in case this
// gets called because the cert expired.
server := c.router.FindLANServer()
if server != nil {
servers = []string{server.Addr.String()}
}
if len(servers) == 0 {
return errFn(fmt.Errorf("No servers to request AutoEncrypt.Sign"))
}
pkPEM, csr, err := c.autoEncryptCSR(extraDNSSANs, extraIPSANs)
if err != nil {
return errFn(err)
}
// Prepare request and response so that it can be passed to
// RPCInsecure.
args := structs.CASignRequest{
WriteRequest: structs.WriteRequest{Token: token},
Datacenter: c.config.Datacenter,
CSR: csr,
}
var reply structs.SignedResponse
// Retry implementation modeled after https://github.com/hashicorp/consul/pull/5228.
// TLDR; there is a 30s window from which a random time is picked.
// Repeat until the call is successful.
attempts := 0
for {
select {
case <-ctx.Done():
return errFn(fmt.Errorf("aborting AutoEncrypt because interrupted: %w", ctx.Err()))
default:
}
// Translate host to net.TCPAddr to make life easier for
// RPCInsecure.
for _, s := range servers {
ips, err := resolveAddr(s, c.logger)
if err != nil {
c.logger.Warn("AutoEncrypt resolveAddr failed", "error", err)
continue
}
for _, ip := range ips {
addr := net.TCPAddr{IP: ip, Port: port}
if err = c.connPool.RPC(c.config.Datacenter, c.config.NodeName, &addr, "AutoEncrypt.Sign", &args, &reply); err == nil {
reply.IssuedCert.PrivateKeyPEM = pkPEM
return &reply, nil
} else {
c.logger.Warn("AutoEncrypt failed", "error", err)
}
}
}
attempts++
delay := lib.RandomStagger(retryJitterWindow)
interval := (time.Duration(attempts) * delay) + delay
c.logger.Warn("retrying AutoEncrypt", "retry_interval", interval)
select {
case <-time.After(interval):
continue
case <-ctx.Done():
return errFn(fmt.Errorf("aborting AutoEncrypt because interrupted: %w", ctx.Err()))
case <-c.shutdownCh:
return errFn(fmt.Errorf("aborting AutoEncrypt because shutting down"))
}
}
}
func missingPortError(host string, err error) bool {
return err != nil && err.Error() == fmt.Sprintf("address %s: missing port in address", host)
}
// resolveAddr is used to resolve the host into IPs and error.
func resolveAddr(rawHost string, logger hclog.Logger) ([]net.IP, error) {
host, _, err := net.SplitHostPort(rawHost)
if err != nil {
// In case we encounter this error, we proceed with the
// rawHost. This is fine since -start-join and -retry-join
// take only hosts anyways and this is an expected case.
if missingPortError(rawHost, err) {
host = rawHost
} else {
return nil, err
}
}
if ip := net.ParseIP(host); ip != nil {
return []net.IP{ip}, nil
}
// First try TCP so we have the best chance for the largest list of
// hosts to join. If this fails it's not fatal since this isn't a standard
// way to query DNS, and we have a fallback below.
if ips, err := tcpLookupIP(host, logger); err != nil {
logger.Debug("TCP-first lookup failed for host, falling back to UDP", "host", host, "error", err)
} else if len(ips) > 0 {
return ips, nil
}
// If TCP didn't yield anything then use the normal Go resolver which
// will try UDP, then might possibly try TCP again if the UDP response
// indicates it was truncated.
ips, err := net.LookupIP(host)
if err != nil {
return nil, err
}
return ips, nil
}
// tcpLookupIP is a helper to initiate a TCP-based DNS lookup for the given host.
// The built-in Go resolver will do a UDP lookup first, and will only use TCP if
// the response has the truncate bit set, which isn't common on DNS servers like
// Consul's. By doing the TCP lookup directly, we get the best chance for the
// largest list of hosts to join. Since joins are relatively rare events, it's ok
// to do this rather expensive operation.
func tcpLookupIP(host string, logger hclog.Logger) ([]net.IP, error) {
// Don't attempt any TCP lookups against non-fully qualified domain
// names, since those will likely come from the resolv.conf file.
if !strings.Contains(host, ".") {
return nil, nil
}
// Make sure the domain name is terminated with a dot (we know there's
// at least one character at this point).
dn := host
if dn[len(dn)-1] != '.' {
dn = dn + "."
}
// See if we can find a server to try.
cc, err := dns.ClientConfigFromFile("/etc/resolv.conf")
if err != nil {
return nil, err
}
if len(cc.Servers) > 0 {
// Do the lookup.
c := new(dns.Client)
c.Net = "tcp"
msg := new(dns.Msg)
msg.SetQuestion(dn, dns.TypeANY)
in, _, err := c.Exchange(msg, cc.Servers[0])
if err != nil {
return nil, err
}
// Handle any IPs we get back that we can attempt to join.
var ips []net.IP
for _, r := range in.Answer {
switch rr := r.(type) {
case (*dns.A):
ips = append(ips, rr.A)
case (*dns.AAAA):
ips = append(ips, rr.AAAA)
case (*dns.CNAME):
logger.Debug("Ignoring CNAME RR in TCP-first answer for host", "host", host)
}
}
return ips, nil
}
return nil, nil
}

View File

@ -1,205 +0,0 @@
package consul
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"net"
"net/url"
"os"
"testing"
"time"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
)
func TestAutoEncrypt_resolveAddr(t *testing.T) {
type args struct {
rawHost string
logger hclog.Logger
}
logger := testutil.Logger(t)
tests := []struct {
name string
args args
ips []net.IP
wantErr bool
}{
{
name: "host without port",
args: args{
"127.0.0.1",
logger,
},
ips: []net.IP{net.IPv4(127, 0, 0, 1)},
wantErr: false,
},
{
name: "host with port",
args: args{
"127.0.0.1:1234",
logger,
},
ips: []net.IP{net.IPv4(127, 0, 0, 1)},
wantErr: false,
},
{
name: "host with broken port",
args: args{
"127.0.0.1:xyz",
logger,
},
ips: []net.IP{net.IPv4(127, 0, 0, 1)},
wantErr: false,
},
{
name: "not an address",
args: args{
"abc",
logger,
},
ips: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ips, err := resolveAddr(tt.args.rawHost, tt.args.logger)
if (err != nil) != tt.wantErr {
t.Errorf("resolveAddr error: %v, wantErr: %v", err, tt.wantErr)
return
}
require.Equal(t, tt.ips, ips)
})
}
}
func TestAutoEncrypt_missingPortError(t *testing.T) {
host := "127.0.0.1"
_, _, err := net.SplitHostPort(host)
require.True(t, missingPortError(host, err))
host = "127.0.0.1:1234"
_, _, err = net.SplitHostPort(host)
require.False(t, missingPortError(host, err))
}
func TestAutoEncrypt_RequestAutoEncryptCerts(t *testing.T) {
dir1, c1 := testClient(t)
defer os.RemoveAll(dir1)
defer c1.Shutdown()
servers := []string{"localhost"}
port := 8301
token := ""
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(75*time.Millisecond))
defer cancel()
doneCh := make(chan struct{})
var err error
go func() {
_, err = c1.RequestAutoEncryptCerts(ctx, servers, port, token, nil, nil)
close(doneCh)
}()
select {
case <-doneCh:
// since there are no servers at this port, we shouldn't be
// done and this should be an error of some sorts that happened
// in the setup phase before entering the for loop in
// RequestAutoEncryptCerts.
require.NoError(t, err)
case <-ctx.Done():
// this is the happy case since auto encrypt is in its loop to
// try to request certs.
}
}
func TestAutoEncrypt_autoEncryptCSR(t *testing.T) {
type testCase struct {
conf *Config
extraDNSSANs []string
extraIPSANs []net.IP
err string
// to validate the csr
expectedSubject pkix.Name
expectedSigAlg x509.SignatureAlgorithm
expectedPubAlg x509.PublicKeyAlgorithm
expectedDNSNames []string
expectedIPs []net.IP
expectedURIs []*url.URL
}
cases := map[string]testCase{
"sans": {
conf: &Config{
Datacenter: "dc1",
NodeName: "test-node",
CAConfig: &structs.CAConfiguration{},
},
extraDNSSANs: []string{"foo.local", "bar.local"},
extraIPSANs: []net.IP{net.IPv4(198, 18, 0, 1), net.IPv4(198, 18, 0, 2)},
expectedSubject: pkix.Name{
CommonName: connect.AgentCN("test-node", dummyTrustDomain),
Names: []pkix.AttributeTypeAndValue{
{
// 2,5,4,3 is the CommonName type ASN1 identifier
Type: asn1.ObjectIdentifier{2, 5, 4, 3},
Value: "testnode.agnt.dummy.tr.consul",
},
},
},
expectedSigAlg: x509.ECDSAWithSHA256,
expectedPubAlg: x509.ECDSA,
expectedDNSNames: []string{
"localhost",
"foo.local",
"bar.local",
},
expectedIPs: []net.IP{
{127, 0, 0, 1},
net.ParseIP("::1"),
{198, 18, 0, 1},
{198, 18, 0, 2},
},
expectedURIs: []*url.URL{
{
Scheme: "spiffe",
Host: dummyTrustDomain,
Path: "/agent/client/dc/dc1/id/test-node",
},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
client := Client{config: tcase.conf}
_, csr, err := client.autoEncryptCSR(tcase.extraDNSSANs, tcase.extraIPSANs)
if tcase.err == "" {
require.NoError(t, err)
request, err := connect.ParseCSR(csr)
require.NoError(t, err)
require.NotNil(t, request)
require.Equal(t, tcase.expectedSubject, request.Subject)
require.Equal(t, tcase.expectedSigAlg, request.SignatureAlgorithm)
require.Equal(t, tcase.expectedPubAlg, request.PublicKeyAlgorithm)
require.Equal(t, tcase.expectedDNSNames, request.DNSNames)
require.Equal(t, tcase.expectedIPs, request.IPAddresses)
require.Equal(t, tcase.expectedURIs, request.URIs)
} else {
require.Error(t, err)
require.Empty(t, csr)
}
})
}
}

View File

@ -443,6 +443,10 @@ type Config struct {
// dead servers. // dead servers.
AutopilotInterval time.Duration AutopilotInterval time.Duration
// MetricsReportingInterval is the frequency with which the server will
// report usage metrics to the configured go-metrics Sinks.
MetricsReportingInterval time.Duration
// ConnectEnabled is whether to enable Connect features such as the CA. // ConnectEnabled is whether to enable Connect features such as the CA.
ConnectEnabled bool ConnectEnabled bool
@ -589,11 +593,16 @@ func DefaultConfig() *Config {
}, },
}, },
ServerHealthInterval: 2 * time.Second, // Stay under the 10 second aggregation interval of
AutopilotInterval: 10 * time.Second, // go-metrics. This ensures we always report the
DefaultQueryTime: 300 * time.Second, // usage metrics in each cycle.
MaxQueryTime: 600 * time.Second, MetricsReportingInterval: 9 * time.Second,
EnterpriseConfig: DefaultEnterpriseConfig(), ServerHealthInterval: 2 * time.Second,
AutopilotInterval: 10 * time.Second,
DefaultQueryTime: 300 * time.Second,
MaxQueryTime: 600 * time.Second,
EnterpriseConfig: DefaultEnterpriseConfig(),
} }
// Increase our reap interval to 3 days instead of 24h. // Increase our reap interval to 3 days instead of 24h.

View File

@ -654,6 +654,12 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, fedState2, fedStateLoaded2) require.Equal(t, fedState2, fedStateLoaded2)
// Verify usage data is correctly updated
idx, nodeCount, err := fsm2.state.NodeCount()
require.NoError(t, err)
require.Equal(t, len(nodes), nodeCount)
require.NotZero(t, idx)
// Snapshot // Snapshot
snap, err = fsm2.Snapshot() snap, err = fsm2.Snapshot()
require.NoError(t, err) require.NoError(t, err)

View File

@ -653,7 +653,7 @@ func (s *Server) secondaryIntermediateCertRenewalWatch(ctx context.Context) erro
case <-ctx.Done(): case <-ctx.Done():
return nil return nil
case <-time.After(structs.IntermediateCertRenewInterval): case <-time.After(structs.IntermediateCertRenewInterval):
retryLoopBackoff(ctx, func() error { retryLoopBackoffAbortOnSuccess(ctx, func() error {
s.caProviderReconfigurationLock.Lock() s.caProviderReconfigurationLock.Lock()
defer s.caProviderReconfigurationLock.Unlock() defer s.caProviderReconfigurationLock.Unlock()
@ -835,6 +835,14 @@ func (s *Server) replicateIntentions(ctx context.Context) error {
// retryLoopBackoff loops a given function indefinitely, backing off exponentially // retryLoopBackoff loops a given function indefinitely, backing off exponentially
// upon errors up to a maximum of maxRetryBackoff seconds. // upon errors up to a maximum of maxRetryBackoff seconds.
func retryLoopBackoff(ctx context.Context, loopFn func() error, errFn func(error)) { func retryLoopBackoff(ctx context.Context, loopFn func() error, errFn func(error)) {
retryLoopBackoffHandleSuccess(ctx, loopFn, errFn, false)
}
func retryLoopBackoffAbortOnSuccess(ctx context.Context, loopFn func() error, errFn func(error)) {
retryLoopBackoffHandleSuccess(ctx, loopFn, errFn, true)
}
func retryLoopBackoffHandleSuccess(ctx context.Context, loopFn func() error, errFn func(error), abortOnSuccess bool) {
var failedAttempts uint var failedAttempts uint
limiter := rate.NewLimiter(loopRateLimit, retryBucketSize) limiter := rate.NewLimiter(loopRateLimit, retryBucketSize)
for { for {
@ -861,6 +869,8 @@ func retryLoopBackoff(ctx context.Context, loopFn func() error, errFn func(error
case <-timer.C: case <-timer.C:
continue continue
} }
} else if abortOnSuccess {
return
} }
// Reset the failed attempts after a successful run. // Reset the failed attempts after a successful run.

View File

@ -1,6 +1,7 @@
package consul package consul
import ( import (
"context"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -1442,3 +1443,43 @@ func TestLeader_lessThanHalfTimePassed(t *testing.T) {
require.True(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now.Add(20*time.Second))) require.True(t, lessThanHalfTimePassed(now, now.Add(-10*time.Second), now.Add(20*time.Second)))
} }
func TestLeader_retryLoopBackoffHandleSuccess(t *testing.T) {
type test struct {
desc string
loopFn func() error
abort bool
timedOut bool
}
success := func() error {
return nil
}
failure := func() error {
return fmt.Errorf("test error")
}
tests := []test{
{"loop without error and no abortOnSuccess keeps running", success, false, true},
{"loop with error and no abortOnSuccess keeps running", failure, false, true},
{"loop without error and abortOnSuccess is stopped", success, true, false},
{"loop with error and abortOnSuccess keeps running", failure, true, true},
}
for _, tc := range tests {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
retryLoopBackoffHandleSuccess(ctx, tc.loopFn, func(_ error) {}, tc.abort)
select {
case <-ctx.Done():
if !tc.timedOut {
t.Fatal("should not have timed out")
}
default:
if tc.timedOut {
t.Fatal("should have timed out")
}
}
})
}
}

View File

@ -25,6 +25,7 @@ import (
"github.com/hashicorp/consul/agent/consul/autopilot" "github.com/hashicorp/consul/agent/consul/autopilot"
"github.com/hashicorp/consul/agent/consul/fsm" "github.com/hashicorp/consul/agent/consul/fsm"
"github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/consul/usagemetrics"
"github.com/hashicorp/consul/agent/metadata" "github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/pool" "github.com/hashicorp/consul/agent/pool"
"github.com/hashicorp/consul/agent/router" "github.com/hashicorp/consul/agent/router"
@ -589,6 +590,19 @@ func NewServer(config *Config, options ...ConsulOption) (*Server, error) {
return nil, err return nil, err
} }
reporter, err := usagemetrics.NewUsageMetricsReporter(
new(usagemetrics.Config).
WithStateProvider(s.fsm).
WithLogger(s.logger).
WithDatacenter(s.config.Datacenter).
WithReportingInterval(s.config.MetricsReportingInterval),
)
if err != nil {
s.Shutdown()
return nil, fmt.Errorf("Failed to start usage metrics reporter: %v", err)
}
go reporter.Run(&lib.StopChannelContext{StopCh: s.shutdownCh})
// Initialize Autopilot. This must happen before starting leadership monitoring // Initialize Autopilot. This must happen before starting leadership monitoring
// as establishing leadership could attempt to use autopilot and cause a panic. // as establishing leadership could attempt to use autopilot and cause a panic.
s.initAutopilot(config) s.initAutopilot(config)

View File

@ -0,0 +1,475 @@
package state
import (
"github.com/hashicorp/consul/agent/consul/stream"
"github.com/hashicorp/consul/agent/structs"
memdb "github.com/hashicorp/go-memdb"
)
type changeOp int
const (
OpDelete changeOp = iota
OpCreate
OpUpdate
)
type eventPayload struct {
Op changeOp
Obj interface{}
}
// serviceHealthSnapshot returns a stream.SnapshotFunc that provides a snapshot
// of stream.Events that describe the current state of a service health query.
//
// TODO: no tests for this yet
func serviceHealthSnapshot(s *Store, topic topic) stream.SnapshotFunc {
return func(req stream.SubscribeRequest, buf stream.SnapshotAppender) (index uint64, err error) {
tx := s.db.Txn(false)
defer tx.Abort()
connect := topic == TopicServiceHealthConnect
// TODO(namespace-streaming): plumb entMeta through from SubscribeRequest
idx, nodes, err := checkServiceNodesTxn(tx, nil, req.Key, connect, nil)
if err != nil {
return 0, err
}
for _, n := range nodes {
event := stream.Event{
Index: idx,
Topic: topic,
Payload: eventPayload{
Op: OpCreate,
Obj: &n,
},
}
if n.Service != nil {
event.Key = n.Service.Service
}
// append each event as a separate item so that they can be serialized
// separately, to prevent the encoding of one massive message.
buf.Append([]stream.Event{event})
}
return idx, err
}
}
type nodeServiceTuple struct {
Node string
ServiceID string
EntMeta structs.EnterpriseMeta
}
func newNodeServiceTupleFromServiceNode(sn *structs.ServiceNode) nodeServiceTuple {
return nodeServiceTuple{
Node: sn.Node,
ServiceID: sn.ServiceID,
EntMeta: sn.EnterpriseMeta,
}
}
func newNodeServiceTupleFromServiceHealthCheck(hc *structs.HealthCheck) nodeServiceTuple {
return nodeServiceTuple{
Node: hc.Node,
ServiceID: hc.ServiceID,
EntMeta: hc.EnterpriseMeta,
}
}
type serviceChange struct {
changeType changeType
change memdb.Change
}
var serviceChangeIndirect = serviceChange{changeType: changeIndirect}
// ServiceHealthEventsFromChanges returns all the service and Connect health
// events that should be emitted given a set of changes to the state store.
func ServiceHealthEventsFromChanges(tx ReadTxn, changes Changes) ([]stream.Event, error) {
var events []stream.Event
var nodeChanges map[string]changeType
var serviceChanges map[nodeServiceTuple]serviceChange
markNode := func(node string, typ changeType) {
if nodeChanges == nil {
nodeChanges = make(map[string]changeType)
}
// If the caller has an actual node mutation ensure we store it even if the
// node is already marked. If the caller is just marking the node dirty
// without a node change, don't overwrite any existing node change we know
// about.
if nodeChanges[node] == changeIndirect {
nodeChanges[node] = typ
}
}
markService := func(key nodeServiceTuple, svcChange serviceChange) {
if serviceChanges == nil {
serviceChanges = make(map[nodeServiceTuple]serviceChange)
}
// If the caller has an actual service mutation ensure we store it even if
// the service is already marked. If the caller is just marking the service
// dirty without a service change, don't overwrite any existing service change we
// know about.
if serviceChanges[key].changeType == changeIndirect {
serviceChanges[key] = svcChange
}
}
for _, change := range changes.Changes {
switch change.Table {
case "nodes":
// Node changed in some way, if it's not a delete, we'll need to
// re-deliver CheckServiceNode results for all services on that node but
// we mark it anyway because if it _is_ a delete then we need to know that
// later to avoid trying to deliver events when node level checks mark the
// node as "changed".
n := changeObject(change).(*structs.Node)
markNode(n.Node, changeTypeFromChange(change))
case "services":
sn := changeObject(change).(*structs.ServiceNode)
srvChange := serviceChange{changeType: changeTypeFromChange(change), change: change}
markService(newNodeServiceTupleFromServiceNode(sn), srvChange)
case "checks":
// For health we only care about the scope for now to know if it's just
// affecting a single service or every service on a node. There is a
// subtle edge case where the check with same ID changes from being node
// scoped to service scoped or vice versa, in either case we need to treat
// it as affecting all services on the node.
switch {
case change.Updated():
before := change.Before.(*structs.HealthCheck)
after := change.After.(*structs.HealthCheck)
if after.ServiceID == "" || before.ServiceID == "" {
// check before and/or after is node-scoped
markNode(after.Node, changeIndirect)
} else {
// Check changed which means we just need to emit for the linked
// service.
markService(newNodeServiceTupleFromServiceHealthCheck(after), serviceChangeIndirect)
// Edge case - if the check with same ID was updated to link to a
// different service ID but the old service with old ID still exists,
// then the old service instance needs updating too as it has one
// fewer checks now.
if before.ServiceID != after.ServiceID {
markService(newNodeServiceTupleFromServiceHealthCheck(before), serviceChangeIndirect)
}
}
case change.Deleted(), change.Created():
obj := changeObject(change).(*structs.HealthCheck)
if obj.ServiceID == "" {
// Node level check
markNode(obj.Node, changeIndirect)
} else {
markService(newNodeServiceTupleFromServiceHealthCheck(obj), serviceChangeIndirect)
}
}
}
}
// Now act on those marked nodes/services
for node, changeType := range nodeChanges {
if changeType == changeDelete {
// Node deletions are a no-op here since the state store transaction will
// have also removed all the service instances which will be handled in
// the loop below.
continue
}
// Rebuild events for all services on this node
es, err := newServiceHealthEventsForNode(tx, changes.Index, node)
if err != nil {
return nil, err
}
events = append(events, es...)
}
for tuple, srvChange := range serviceChanges {
// change may be nil if there was a change that _affected_ the service
// like a change to checks but it didn't actually change the service
// record itself.
if srvChange.changeType == changeDelete {
sn := srvChange.change.Before.(*structs.ServiceNode)
e := newServiceHealthEventDeregister(changes.Index, sn)
events = append(events, e)
continue
}
// Check if this was a service mutation that changed it's name which
// requires special handling even if node changed and new events were
// already published.
if srvChange.changeType == changeUpdate {
before := srvChange.change.Before.(*structs.ServiceNode)
after := srvChange.change.After.(*structs.ServiceNode)
if before.ServiceName != after.ServiceName {
// Service was renamed, the code below will ensure the new registrations
// go out to subscribers to the new service name topic key, but we need
// to fix up subscribers that were watching the old name by sending
// deregistrations.
e := newServiceHealthEventDeregister(changes.Index, before)
events = append(events, e)
}
if e, ok := isConnectProxyDestinationServiceChange(changes.Index, before, after); ok {
events = append(events, e)
}
}
if _, ok := nodeChanges[tuple.Node]; ok {
// We already rebuilt events for everything on this node, no need to send
// a duplicate.
continue
}
// Build service event and append it
e, err := newServiceHealthEventForService(tx, changes.Index, tuple)
if err != nil {
return nil, err
}
events = append(events, e)
}
// Duplicate any events that affected connect-enabled instances (proxies or
// native apps) to the relevant Connect topic.
events = append(events, serviceHealthToConnectEvents(events...)...)
return events, nil
}
// isConnectProxyDestinationServiceChange handles the case where a Connect proxy changed
// the service it is proxying. We need to issue a de-registration for the old
// service on the Connect topic. We don't actually need to deregister this sidecar
// service though as it still exists and didn't change its name.
func isConnectProxyDestinationServiceChange(idx uint64, before, after *structs.ServiceNode) (stream.Event, bool) {
if before.ServiceKind != structs.ServiceKindConnectProxy ||
before.ServiceProxy.DestinationServiceName == after.ServiceProxy.DestinationServiceName {
return stream.Event{}, false
}
e := newServiceHealthEventDeregister(idx, before)
e.Topic = TopicServiceHealthConnect
e.Key = getPayloadCheckServiceNode(e.Payload).Service.Proxy.DestinationServiceName
return e, true
}
type changeType uint8
const (
// changeIndirect indicates some other object changed which has implications
// for the target object.
changeIndirect changeType = iota
changeDelete
changeCreate
changeUpdate
)
func changeTypeFromChange(change memdb.Change) changeType {
switch {
case change.Deleted():
return changeDelete
case change.Created():
return changeCreate
default:
return changeUpdate
}
}
// serviceHealthToConnectEvents converts already formatted service health
// registration events into the ones needed to publish to the Connect topic.
// This essentially means filtering out any instances that are not Connect
// enabled and so of no interest to those subscribers but also involves
// switching connection details to be the proxy instead of the actual instance
// in case of a sidecar.
func serviceHealthToConnectEvents(events ...stream.Event) []stream.Event {
var result []stream.Event
for _, event := range events {
if event.Topic != TopicServiceHealth {
// Skip non-health or any events already emitted to Connect topic
continue
}
node := getPayloadCheckServiceNode(event.Payload)
if node.Service == nil {
continue
}
connectEvent := event
connectEvent.Topic = TopicServiceHealthConnect
switch {
case node.Service.Connect.Native:
result = append(result, connectEvent)
case node.Service.Kind == structs.ServiceKindConnectProxy:
connectEvent.Key = node.Service.Proxy.DestinationServiceName
result = append(result, connectEvent)
default:
// ServiceKindTerminatingGateway changes are handled separately.
// All other cases are not relevant to the connect topic
}
}
return result
}
func getPayloadCheckServiceNode(payload interface{}) *structs.CheckServiceNode {
ep, ok := payload.(eventPayload)
if !ok {
return nil
}
csn, ok := ep.Obj.(*structs.CheckServiceNode)
if !ok {
return nil
}
return csn
}
// newServiceHealthEventsForNode returns health events for all services on the
// given node. This mirrors some of the the logic in the oddly-named
// parseCheckServiceNodes but is more efficient since we know they are all on
// the same node.
func newServiceHealthEventsForNode(tx ReadTxn, idx uint64, node string) ([]stream.Event, error) {
// TODO(namespace-streaming): figure out the right EntMeta and mystery arg.
services, err := catalogServiceListByNode(tx, node, nil, false)
if err != nil {
return nil, err
}
n, checksFunc, err := getNodeAndChecks(tx, node)
if err != nil {
return nil, err
}
var events []stream.Event
for service := services.Next(); service != nil; service = services.Next() {
sn := service.(*structs.ServiceNode)
event := newServiceHealthEventRegister(idx, n, sn, checksFunc(sn.ServiceID))
events = append(events, event)
}
return events, nil
}
// getNodeAndNodeChecks returns a the node structure and a function that returns
// the full list of checks for a specific service on that node.
func getNodeAndChecks(tx ReadTxn, node string) (*structs.Node, serviceChecksFunc, error) {
// Fetch the node
nodeRaw, err := tx.First("nodes", "id", node)
if err != nil {
return nil, nil, err
}
if nodeRaw == nil {
return nil, nil, ErrMissingNode
}
n := nodeRaw.(*structs.Node)
// TODO(namespace-streaming): work out what EntMeta is needed here, wildcard?
iter, err := catalogListChecksByNode(tx, node, nil)
if err != nil {
return nil, nil, err
}
var nodeChecks structs.HealthChecks
var svcChecks map[string]structs.HealthChecks
for check := iter.Next(); check != nil; check = iter.Next() {
check := check.(*structs.HealthCheck)
if check.ServiceID == "" {
nodeChecks = append(nodeChecks, check)
} else {
if svcChecks == nil {
svcChecks = make(map[string]structs.HealthChecks)
}
svcChecks[check.ServiceID] = append(svcChecks[check.ServiceID], check)
}
}
serviceChecks := func(serviceID string) structs.HealthChecks {
// Create a new slice so that append does not modify the array backing nodeChecks.
result := make(structs.HealthChecks, 0, len(nodeChecks))
result = append(result, nodeChecks...)
for _, check := range svcChecks[serviceID] {
result = append(result, check)
}
return result
}
return n, serviceChecks, nil
}
type serviceChecksFunc func(serviceID string) structs.HealthChecks
func newServiceHealthEventForService(tx ReadTxn, idx uint64, tuple nodeServiceTuple) (stream.Event, error) {
n, checksFunc, err := getNodeAndChecks(tx, tuple.Node)
if err != nil {
return stream.Event{}, err
}
svc, err := getCompoundWithTxn(tx, "services", "id", &tuple.EntMeta, tuple.Node, tuple.ServiceID)
if err != nil {
return stream.Event{}, err
}
raw := svc.Next()
if raw == nil {
return stream.Event{}, ErrMissingService
}
sn := raw.(*structs.ServiceNode)
return newServiceHealthEventRegister(idx, n, sn, checksFunc(sn.ServiceID)), nil
}
func newServiceHealthEventRegister(
idx uint64,
node *structs.Node,
sn *structs.ServiceNode,
checks structs.HealthChecks,
) stream.Event {
csn := &structs.CheckServiceNode{
Node: node,
Service: sn.ToNodeService(),
Checks: checks,
}
return stream.Event{
Topic: TopicServiceHealth,
Key: sn.ServiceName,
Index: idx,
Payload: eventPayload{
Op: OpCreate,
Obj: csn,
},
}
}
func newServiceHealthEventDeregister(idx uint64, sn *structs.ServiceNode) stream.Event {
// We actually only need the node name populated in the node part as it's only
// used as a key to know which service was deregistered so don't bother looking
// up the node in the DB. Note that while the ServiceNode does have NodeID
// etc. fields, they are never populated in memdb per the comment on that
// struct and only filled in when we return copies of the result to users.
// This is also important because if the service was deleted as part of a
// whole node deregistering then the node record won't actually exist now
// anyway and we'd have to plumb it through from the changeset above.
csn := &structs.CheckServiceNode{
Node: &structs.Node{
Node: sn.Node,
},
Service: sn.ToNodeService(),
}
return stream.Event{
Topic: TopicServiceHealth,
Key: sn.ServiceName,
Index: idx,
Payload: eventPayload{
Op: OpDelete,
Obj: csn,
},
}
}

File diff suppressed because it is too large Load Diff

View File

@ -467,7 +467,7 @@ func validateProposedConfigEntryInServiceGraph(
} }
overrides := map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides := map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: kind, Name: name}: next, structs.NewConfigEntryKindName(kind, name, entMeta): next,
} }
var ( var (
@ -909,9 +909,8 @@ func configEntryWithOverridesTxn(
entMeta *structs.EnterpriseMeta, entMeta *structs.EnterpriseMeta,
) (uint64, structs.ConfigEntry, error) { ) (uint64, structs.ConfigEntry, error) {
if len(overrides) > 0 { if len(overrides) > 0 {
entry, ok := overrides[structs.ConfigEntryKindName{ kn := structs.NewConfigEntryKindName(kind, name, entMeta)
Kind: kind, Name: name, entry, ok := overrides[kn]
}]
if ok { if ok {
return 0, entry, nil // a nil entry implies it should act like it is erased return 0, entry, nil // a nil entry implies it should act like it is erased
} }

View File

@ -880,10 +880,10 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceDefaults, Name: "main"}: nil, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil): nil,
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
// nothing // nothing
@ -899,17 +899,17 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceDefaults, Name: "main"}: &structs.ServiceConfigEntry{ structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil): &structs.ServiceConfigEntry{
Kind: structs.ServiceDefaults, Kind: structs.ServiceDefaults,
Name: "main", Name: "main",
Protocol: "grpc", Protocol: "grpc",
}, },
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
}, },
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
defaults := entrySet.GetService(structs.NewServiceID("main", nil)) defaults := entrySet.GetService(structs.NewServiceID("main", nil))
@ -932,14 +932,14 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
{Kind: structs.ServiceRouter, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceRouter, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceRouter, Name: "main"}: nil, structs.NewConfigEntryKindName(structs.ServiceRouter, "main", nil): nil,
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
}, },
}, },
{ {
@ -977,12 +977,12 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
{Kind: structs.ServiceResolver, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil),
{Kind: structs.ServiceRouter, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceRouter, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceRouter, Name: "main"}: &structs.ServiceRouterConfigEntry{ structs.NewConfigEntryKindName(structs.ServiceRouter, "main", nil): &structs.ServiceRouterConfigEntry{
Kind: structs.ServiceRouter, Kind: structs.ServiceRouter,
Name: "main", Name: "main",
Routes: []structs.ServiceRoute{ Routes: []structs.ServiceRoute{
@ -1000,9 +1000,9 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
{Kind: structs.ServiceResolver, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil),
{Kind: structs.ServiceRouter, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceRouter, "main", nil),
}, },
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
router := entrySet.GetRouter(structs.NewServiceID("main", nil)) router := entrySet.GetRouter(structs.NewServiceID("main", nil))
@ -1040,14 +1040,14 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
{Kind: structs.ServiceSplitter, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceSplitter, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceSplitter, Name: "main"}: nil, structs.NewConfigEntryKindName(structs.ServiceSplitter, "main", nil): nil,
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
}, },
}, },
{ {
@ -1067,11 +1067,11 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
{Kind: structs.ServiceSplitter, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceSplitter, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceSplitter, Name: "main"}: &structs.ServiceSplitterConfigEntry{ structs.NewConfigEntryKindName(structs.ServiceSplitter, "main", nil): &structs.ServiceSplitterConfigEntry{
Kind: structs.ServiceSplitter, Kind: structs.ServiceSplitter,
Name: "main", Name: "main",
Splits: []structs.ServiceSplit{ Splits: []structs.ServiceSplit{
@ -1081,8 +1081,8 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
{Kind: structs.ServiceDefaults, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceDefaults, "main", nil),
{Kind: structs.ServiceSplitter, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceSplitter, "main", nil),
}, },
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
splitter := entrySet.GetSplitter(structs.NewServiceID("main", nil)) splitter := entrySet.GetSplitter(structs.NewServiceID("main", nil))
@ -1106,10 +1106,10 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceResolver, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceResolver, Name: "main"}: nil, structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil): nil,
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
// nothing // nothing
@ -1124,17 +1124,17 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
}, },
}, },
expectBefore: []structs.ConfigEntryKindName{ expectBefore: []structs.ConfigEntryKindName{
{Kind: structs.ServiceResolver, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil),
}, },
overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{ overrides: map[structs.ConfigEntryKindName]structs.ConfigEntry{
{Kind: structs.ServiceResolver, Name: "main"}: &structs.ServiceResolverConfigEntry{ structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil): &structs.ServiceResolverConfigEntry{
Kind: structs.ServiceResolver, Kind: structs.ServiceResolver,
Name: "main", Name: "main",
ConnectTimeout: 33 * time.Second, ConnectTimeout: 33 * time.Second,
}, },
}, },
expectAfter: []structs.ConfigEntryKindName{ expectAfter: []structs.ConfigEntryKindName{
{Kind: structs.ServiceResolver, Name: "main"}, structs.NewConfigEntryKindName(structs.ServiceResolver, "main", nil),
}, },
checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) { checkAfter: func(t *testing.T, entrySet *structs.DiscoveryChainConfigEntries) {
resolver := entrySet.GetResolver(structs.NewServiceID("main", nil)) resolver := entrySet.GetResolver(structs.NewServiceID("main", nil))
@ -1181,28 +1181,32 @@ func TestStore_ReadDiscoveryChainConfigEntries_Overrides(t *testing.T) {
func entrySetToKindNames(entrySet *structs.DiscoveryChainConfigEntries) []structs.ConfigEntryKindName { func entrySetToKindNames(entrySet *structs.DiscoveryChainConfigEntries) []structs.ConfigEntryKindName {
var out []structs.ConfigEntryKindName var out []structs.ConfigEntryKindName
for _, entry := range entrySet.Routers { for _, entry := range entrySet.Routers {
out = append(out, structs.ConfigEntryKindName{ out = append(out, structs.NewConfigEntryKindName(
Kind: entry.Kind, entry.Kind,
Name: entry.Name, entry.Name,
}) &entry.EnterpriseMeta,
))
} }
for _, entry := range entrySet.Splitters { for _, entry := range entrySet.Splitters {
out = append(out, structs.ConfigEntryKindName{ out = append(out, structs.NewConfigEntryKindName(
Kind: entry.Kind, entry.Kind,
Name: entry.Name, entry.Name,
}) &entry.EnterpriseMeta,
))
} }
for _, entry := range entrySet.Resolvers { for _, entry := range entrySet.Resolvers {
out = append(out, structs.ConfigEntryKindName{ out = append(out, structs.NewConfigEntryKindName(
Kind: entry.Kind, entry.Kind,
Name: entry.Name, entry.Name,
}) &entry.EnterpriseMeta,
))
} }
for _, entry := range entrySet.Services { for _, entry := range entrySet.Services {
out = append(out, structs.ConfigEntryKindName{ out = append(out, structs.NewConfigEntryKindName(
Kind: entry.Kind, entry.Kind,
Name: entry.Name, entry.Name,
}) &entry.EnterpriseMeta,
))
} }
return out return out
} }

View File

@ -15,6 +15,13 @@ type ReadTxn interface {
Abort() Abort()
} }
// WriteTxn is implemented by memdb.Txn to perform write operations.
type WriteTxn interface {
ReadTxn
Insert(table string, obj interface{}) error
Commit() error
}
// Changes wraps a memdb.Changes to include the index at which these changes // Changes wraps a memdb.Changes to include the index at which these changes
// were made. // were made.
type Changes struct { type Changes struct {
@ -24,8 +31,9 @@ type Changes struct {
} }
// changeTrackerDB is a thin wrapper around memdb.DB which enables TrackChanges on // changeTrackerDB is a thin wrapper around memdb.DB which enables TrackChanges on
// all write transactions. When the transaction is committed the changes are // all write transactions. When the transaction is committed the changes are:
// sent to the eventPublisher which will create and emit change events. // 1. Used to update our internal usage tracking
// 2. Sent to the eventPublisher which will create and emit change events
type changeTrackerDB struct { type changeTrackerDB struct {
db *memdb.MemDB db *memdb.MemDB
publisher eventPublisher publisher eventPublisher
@ -77,11 +85,8 @@ func (c *changeTrackerDB) WriteTxn(idx uint64) *txn {
return t return t
} }
func (c *changeTrackerDB) publish(changes Changes) error { func (c *changeTrackerDB) publish(tx ReadTxn, changes Changes) error {
readOnlyTx := c.db.Txn(false) events, err := c.processChanges(tx, changes)
defer readOnlyTx.Abort()
events, err := c.processChanges(readOnlyTx, changes)
if err != nil { if err != nil {
return fmt.Errorf("failed generating events from changes: %v", err) return fmt.Errorf("failed generating events from changes: %v", err)
} }
@ -89,17 +94,21 @@ func (c *changeTrackerDB) publish(changes Changes) error {
return nil return nil
} }
// WriteTxnRestore returns a wrapped RW transaction that does NOT have change // WriteTxnRestore returns a wrapped RW transaction that should only be used in
// tracking enabled. This should only be used in Restore where we need to // Restore where we need to replace the entire contents of the Store.
// replace the entire contents of the Store without a need to track the changes. // WriteTxnRestore uses a zero index since the whole restore doesn't really
// WriteTxnRestore uses a zero index since the whole restore doesn't really occur // occur at one index - the effect is to write many values that were previously
// at one index - the effect is to write many values that were previously // written across many indexes. WriteTxnRestore also does not publish any
// written across many indexes. // change events to subscribers.
func (c *changeTrackerDB) WriteTxnRestore() *txn { func (c *changeTrackerDB) WriteTxnRestore() *txn {
return &txn{ t := &txn{
Txn: c.db.Txn(true), Txn: c.db.Txn(true),
Index: 0, Index: 0,
} }
// We enable change tracking so that usage data is correctly populated.
t.Txn.TrackChanges()
return t
} }
// txn wraps a memdb.Txn to capture changes and send them to the EventPublisher. // txn wraps a memdb.Txn to capture changes and send them to the EventPublisher.
@ -115,7 +124,7 @@ type txn struct {
// Index is stored so that it may be passed along to any subscribers as part // Index is stored so that it may be passed along to any subscribers as part
// of a change event. // of a change event.
Index uint64 Index uint64
publish func(changes Changes) error publish func(tx ReadTxn, changes Changes) error
} }
// Commit first pushes changes to EventPublisher, then calls Commit on the // Commit first pushes changes to EventPublisher, then calls Commit on the
@ -125,15 +134,22 @@ type txn struct {
// by the caller. A non-nil error indicates that a commit failed and was not // by the caller. A non-nil error indicates that a commit failed and was not
// applied. // applied.
func (tx *txn) Commit() error { func (tx *txn) Commit() error {
changes := Changes{
Index: tx.Index,
Changes: tx.Txn.Changes(),
}
if len(changes.Changes) > 0 {
if err := updateUsage(tx, changes); err != nil {
return err
}
}
// publish may be nil if this is a read-only or WriteTxnRestore transaction. // publish may be nil if this is a read-only or WriteTxnRestore transaction.
// In those cases changes should also be empty, and there will be nothing // In those cases changes should also be empty, and there will be nothing
// to publish. // to publish.
if tx.publish != nil { if tx.publish != nil {
changes := Changes{ if err := tx.publish(tx.Txn, changes); err != nil {
Index: tx.Index,
Changes: tx.Txn.Changes(),
}
if err := tx.publish(changes); err != nil {
return err return err
} }
} }
@ -149,11 +165,33 @@ func (t topic) String() string {
return string(t) return string(t)
} }
var (
// TopicServiceHealth contains events for all registered service instances.
TopicServiceHealth topic = "topic-service-health"
// TopicServiceHealthConnect contains events for connect-enabled service instances.
TopicServiceHealthConnect topic = "topic-service-health-connect"
)
func processDBChanges(tx ReadTxn, changes Changes) ([]stream.Event, error) { func processDBChanges(tx ReadTxn, changes Changes) ([]stream.Event, error) {
// TODO: add other table handlers here. var events []stream.Event
return aclChangeUnsubscribeEvent(tx, changes) fns := []func(tx ReadTxn, changes Changes) ([]stream.Event, error){
aclChangeUnsubscribeEvent,
ServiceHealthEventsFromChanges,
// TODO: add other table handlers here.
}
for _, fn := range fns {
e, err := fn(tx, changes)
if err != nil {
return nil, err
}
events = append(events, e...)
}
return events, nil
} }
func newSnapshotHandlers() stream.SnapshotHandlers { func newSnapshotHandlers(s *Store) stream.SnapshotHandlers {
return stream.SnapshotHandlers{} return stream.SnapshotHandlers{
TopicServiceHealth: serviceHealthSnapshot(s, TopicServiceHealth),
TopicServiceHealthConnect: serviceHealthSnapshot(s, TopicServiceHealthConnect),
}
} }

View File

@ -7,30 +7,30 @@ import (
"github.com/hashicorp/go-memdb" "github.com/hashicorp/go-memdb"
) )
func firstWithTxn(tx *txn, func firstWithTxn(tx ReadTxn,
table, index, idxVal string, entMeta *structs.EnterpriseMeta) (interface{}, error) { table, index, idxVal string, entMeta *structs.EnterpriseMeta) (interface{}, error) {
return tx.First(table, index, idxVal) return tx.First(table, index, idxVal)
} }
func firstWatchWithTxn(tx *txn, func firstWatchWithTxn(tx ReadTxn,
table, index, idxVal string, entMeta *structs.EnterpriseMeta) (<-chan struct{}, interface{}, error) { table, index, idxVal string, entMeta *structs.EnterpriseMeta) (<-chan struct{}, interface{}, error) {
return tx.FirstWatch(table, index, idxVal) return tx.FirstWatch(table, index, idxVal)
} }
func firstWatchCompoundWithTxn(tx *txn, func firstWatchCompoundWithTxn(tx ReadTxn,
table, index string, _ *structs.EnterpriseMeta, idxVals ...interface{}) (<-chan struct{}, interface{}, error) { table, index string, _ *structs.EnterpriseMeta, idxVals ...interface{}) (<-chan struct{}, interface{}, error) {
return tx.FirstWatch(table, index, idxVals...) return tx.FirstWatch(table, index, idxVals...)
} }
func getWithTxn(tx *txn, func getWithTxn(tx ReadTxn,
table, index, idxVal string, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) { table, index, idxVal string, entMeta *structs.EnterpriseMeta) (memdb.ResultIterator, error) {
return tx.Get(table, index, idxVal) return tx.Get(table, index, idxVal)
} }
func getCompoundWithTxn(tx *txn, table, index string, func getCompoundWithTxn(tx ReadTxn, table, index string,
_ *structs.EnterpriseMeta, idxVals ...interface{}) (memdb.ResultIterator, error) { _ *structs.EnterpriseMeta, idxVals ...interface{}) (memdb.ResultIterator, error) {
return tx.Get(table, index, idxVals...) return tx.Get(table, index, idxVals...)

View File

@ -162,17 +162,17 @@ func NewStateStore(gc *TombstoneGC) (*Store, error) {
ctx, cancel := context.WithCancel(context.TODO()) ctx, cancel := context.WithCancel(context.TODO())
s := &Store{ s := &Store{
schema: schema, schema: schema,
abandonCh: make(chan struct{}), abandonCh: make(chan struct{}),
kvsGraveyard: NewGraveyard(gc), kvsGraveyard: NewGraveyard(gc),
lockDelay: NewDelay(), lockDelay: NewDelay(),
db: &changeTrackerDB{
db: db,
publisher: stream.NewEventPublisher(ctx, newSnapshotHandlers(), 10*time.Second),
processChanges: processDBChanges,
},
stopEventPublisher: cancel, stopEventPublisher: cancel,
} }
s.db = &changeTrackerDB{
db: db,
publisher: stream.NewEventPublisher(ctx, newSnapshotHandlers(s), 10*time.Second),
processChanges: processDBChanges,
}
return s, nil return s, nil
} }

View File

@ -376,7 +376,7 @@ var topicService stream.Topic = topic("test-topic-service")
func newTestSnapshotHandlers(s *Store) stream.SnapshotHandlers { func newTestSnapshotHandlers(s *Store) stream.SnapshotHandlers {
return stream.SnapshotHandlers{ return stream.SnapshotHandlers{
topicService: func(req *stream.SubscribeRequest, snap stream.SnapshotAppender) (uint64, error) { topicService: func(req stream.SubscribeRequest, snap stream.SnapshotAppender) (uint64, error) {
idx, nodes, err := s.ServiceNodes(nil, req.Key, nil) idx, nodes, err := s.ServiceNodes(nil, req.Key, nil)
if err != nil { if err != nil {
return idx, err return idx, err

258
agent/consul/state/usage.go Normal file
View File

@ -0,0 +1,258 @@
package state
import (
"fmt"
"github.com/hashicorp/consul/agent/structs"
memdb "github.com/hashicorp/go-memdb"
)
const (
serviceNamesUsageTable = "service-names"
)
// usageTableSchema returns a new table schema used for tracking various indexes
// for the Raft log.
func usageTableSchema() *memdb.TableSchema {
return &memdb.TableSchema{
Name: "usage",
Indexes: map[string]*memdb.IndexSchema{
"id": {
Name: "id",
AllowMissing: false,
Unique: true,
Indexer: &memdb.StringFieldIndex{
Field: "ID",
Lowercase: true,
},
},
},
}
}
func init() {
registerSchema(usageTableSchema)
}
// UsageEntry represents a count of some arbitrary identifier within the
// state store, along with the last seen index.
type UsageEntry struct {
ID string
Index uint64
Count int
}
// ServiceUsage contains all of the usage data related to services
type ServiceUsage struct {
Services int
ServiceInstances int
EnterpriseServiceUsage
}
type uniqueServiceState int
const (
NoChange uniqueServiceState = 0
Deleted uniqueServiceState = 1
Created uniqueServiceState = 2
)
// updateUsage takes a set of memdb changes and computes a delta for specific
// usage metrics that we track.
func updateUsage(tx WriteTxn, changes Changes) error {
usageDeltas := make(map[string]int)
for _, change := range changes.Changes {
var delta int
if change.Created() {
delta = 1
} else if change.Deleted() {
delta = -1
}
switch change.Table {
case "nodes":
usageDeltas[change.Table] += delta
case "services":
svc := changeObject(change).(*structs.ServiceNode)
usageDeltas[change.Table] += delta
serviceIter, err := getWithTxn(tx, servicesTableName, "service", svc.ServiceName, &svc.EnterpriseMeta)
if err != nil {
return err
}
var serviceState uniqueServiceState
if serviceIter.Next() == nil {
// If no services exist, we know we deleted the last service
// instance.
serviceState = Deleted
usageDeltas[serviceNamesUsageTable] -= 1
} else if serviceIter.Next() == nil {
// If a second call to Next() returns nil, we know only a single
// instance exists. If, in addition, a new service name has been
// registered, either via creating a new service instance or via
// renaming an existing service, than we update our service count.
//
// We only care about two cases here:
// 1. A new service instance has been created with a unique name
// 2. An existing service instance has been updated with a new unique name
//
// These are the only ways a new unique service can be created. The
// other valid cases here: an update that does not change the service
// name, and a deletion, both do not impact the count of unique service
// names in the system.
if change.Created() {
// Given a single existing service instance of the service: If a
// service has just been created, then we know this is a new unique
// service.
serviceState = Created
usageDeltas[serviceNamesUsageTable] += 1
} else if serviceNameChanged(change) {
// Given a single existing service instance of the service: If a
// service has been updated with a new service name, then we know
// this is a new unique service.
serviceState = Created
usageDeltas[serviceNamesUsageTable] += 1
// Check whether the previous name was deleted in this rename, this
// is a special case of renaming a service which does not result in
// changing the count of unique service names.
before := change.Before.(*structs.ServiceNode)
beforeSvc, err := firstWithTxn(tx, servicesTableName, "service", before.ServiceName, &before.EnterpriseMeta)
if err != nil {
return err
}
if beforeSvc == nil {
usageDeltas[serviceNamesUsageTable] -= 1
// set serviceState to NoChange since we have both gained and lost a
// service, cancelling each other out
serviceState = NoChange
}
}
}
addEnterpriseServiceUsage(usageDeltas, change, serviceState)
}
}
idx := changes.Index
// This will happen when restoring from a snapshot, just take the max index
// of the tables we are tracking.
if idx == 0 {
idx = maxIndexTxn(tx, "nodes", servicesTableName)
}
return writeUsageDeltas(tx, idx, usageDeltas)
}
// serviceNameChanged returns a boolean that indicates whether the
// provided change resulted in an update to the service's service name.
func serviceNameChanged(change memdb.Change) bool {
if change.Updated() {
before := change.Before.(*structs.ServiceNode)
after := change.After.(*structs.ServiceNode)
return before.ServiceName != after.ServiceName
}
return false
}
// writeUsageDeltas will take in a map of IDs to deltas and update each
// entry accordingly, checking for integer underflow. The index that is
// passed in will be recorded on the entry as well.
func writeUsageDeltas(tx WriteTxn, idx uint64, usageDeltas map[string]int) error {
for id, delta := range usageDeltas {
u, err := tx.First("usage", "id", id)
if err != nil {
return fmt.Errorf("failed to retrieve existing usage entry: %s", err)
}
if u == nil {
if delta < 0 {
return fmt.Errorf("failed to insert usage entry for %q: delta will cause a negative count", id)
}
err := tx.Insert("usage", &UsageEntry{
ID: id,
Count: delta,
Index: idx,
})
if err != nil {
return fmt.Errorf("failed to update usage entry: %s", err)
}
} else if cur, ok := u.(*UsageEntry); ok {
if cur.Count+delta < 0 {
return fmt.Errorf("failed to insert usage entry for %q: delta will cause a negative count", id)
}
err := tx.Insert("usage", &UsageEntry{
ID: id,
Count: cur.Count + delta,
Index: idx,
})
if err != nil {
return fmt.Errorf("failed to update usage entry: %s", err)
}
}
}
return nil
}
// NodeCount returns the latest seen Raft index, a count of the number of nodes
// registered, and any errors.
func (s *Store) NodeCount() (uint64, int, error) {
tx := s.db.ReadTxn()
defer tx.Abort()
nodeUsage, err := firstUsageEntry(tx, "nodes")
if err != nil {
return 0, 0, fmt.Errorf("failed nodes lookup: %s", err)
}
return nodeUsage.Index, nodeUsage.Count, nil
}
// ServiceUsage returns the latest seen Raft index, a compiled set of service
// usage data, and any errors.
func (s *Store) ServiceUsage() (uint64, ServiceUsage, error) {
tx := s.db.ReadTxn()
defer tx.Abort()
serviceInstances, err := firstUsageEntry(tx, servicesTableName)
if err != nil {
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
}
services, err := firstUsageEntry(tx, serviceNamesUsageTable)
if err != nil {
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
}
usage := ServiceUsage{
ServiceInstances: serviceInstances.Count,
Services: services.Count,
}
results, err := compileEnterpriseUsage(tx, usage)
if err != nil {
return 0, ServiceUsage{}, fmt.Errorf("failed services lookup: %s", err)
}
return serviceInstances.Index, results, nil
}
func firstUsageEntry(tx ReadTxn, id string) (*UsageEntry, error) {
usage, err := tx.First("usage", "id", id)
if err != nil {
return nil, err
}
// If no elements have been inserted, the usage entry will not exist. We
// return a valid value so that can be certain the return value is not nil
// when no error has occurred.
if usage == nil {
return &UsageEntry{ID: id, Count: 0}, nil
}
realUsage, ok := usage.(*UsageEntry)
if !ok {
return nil, fmt.Errorf("failed usage lookup: type %T is not *UsageEntry", usage)
}
return realUsage, nil
}

View File

@ -0,0 +1,15 @@
// +build !consulent
package state
import (
memdb "github.com/hashicorp/go-memdb"
)
type EnterpriseServiceUsage struct{}
func addEnterpriseServiceUsage(map[string]int, memdb.Change, uniqueServiceState) {}
func compileEnterpriseUsage(tx ReadTxn, usage ServiceUsage) (ServiceUsage, error) {
return usage, nil
}

View File

@ -0,0 +1,25 @@
// +build !consulent
package state
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestStateStore_Usage_ServiceUsage(t *testing.T) {
s := testStateStore(t)
testRegisterNode(t, s, 0, "node1")
testRegisterNode(t, s, 1, "node2")
testRegisterService(t, s, 8, "node1", "service1")
testRegisterService(t, s, 9, "node2", "service1")
testRegisterService(t, s, 10, "node2", "service2")
idx, usage, err := s.ServiceUsage()
require.NoError(t, err)
require.Equal(t, idx, uint64(10))
require.Equal(t, 2, usage.Services)
require.Equal(t, 3, usage.ServiceInstances)
}

View File

@ -0,0 +1,194 @@
package state
import (
"testing"
"github.com/hashicorp/consul/agent/structs"
memdb "github.com/hashicorp/go-memdb"
"github.com/stretchr/testify/require"
)
func TestStateStore_Usage_NodeCount(t *testing.T) {
s := testStateStore(t)
// No nodes have been registered, and thus no usage entry exists
idx, count, err := s.NodeCount()
require.NoError(t, err)
require.Equal(t, idx, uint64(0))
require.Equal(t, count, 0)
testRegisterNode(t, s, 0, "node1")
testRegisterNode(t, s, 1, "node2")
idx, count, err = s.NodeCount()
require.NoError(t, err)
require.Equal(t, idx, uint64(1))
require.Equal(t, count, 2)
}
func TestStateStore_Usage_NodeCount_Delete(t *testing.T) {
s := testStateStore(t)
testRegisterNode(t, s, 0, "node1")
testRegisterNode(t, s, 1, "node2")
idx, count, err := s.NodeCount()
require.NoError(t, err)
require.Equal(t, idx, uint64(1))
require.Equal(t, count, 2)
require.NoError(t, s.DeleteNode(2, "node2"))
idx, count, err = s.NodeCount()
require.NoError(t, err)
require.Equal(t, idx, uint64(2))
require.Equal(t, count, 1)
}
func TestStateStore_Usage_ServiceUsageEmpty(t *testing.T) {
s := testStateStore(t)
// No services have been registered, and thus no usage entry exists
idx, usage, err := s.ServiceUsage()
require.NoError(t, err)
require.Equal(t, idx, uint64(0))
require.Equal(t, usage.Services, 0)
require.Equal(t, usage.ServiceInstances, 0)
}
func TestStateStore_Usage_Restore(t *testing.T) {
s := testStateStore(t)
restore := s.Restore()
restore.Registration(9, &structs.RegisterRequest{
Node: "test-node",
Service: &structs.NodeService{
ID: "mysql",
Service: "mysql",
Port: 8080,
Address: "198.18.0.2",
},
})
require.NoError(t, restore.Commit())
idx, count, err := s.NodeCount()
require.NoError(t, err)
require.Equal(t, idx, uint64(9))
require.Equal(t, count, 1)
}
func TestStateStore_Usage_updateUsage_Underflow(t *testing.T) {
s := testStateStore(t)
txn := s.db.WriteTxn(1)
// A single delete change will cause a negative count
changes := Changes{
Index: 1,
Changes: memdb.Changes{
{
Table: "nodes",
Before: &structs.Node{},
After: nil,
},
},
}
err := updateUsage(txn, changes)
require.Error(t, err)
require.Contains(t, err.Error(), "negative count")
// A insert a change to create a usage entry
changes = Changes{
Index: 1,
Changes: memdb.Changes{
{
Table: "nodes",
Before: nil,
After: &structs.Node{},
},
},
}
err = updateUsage(txn, changes)
require.NoError(t, err)
// Two deletes will cause a negative count now
changes = Changes{
Index: 1,
Changes: memdb.Changes{
{
Table: "nodes",
Before: &structs.Node{},
After: nil,
},
{
Table: "nodes",
Before: &structs.Node{},
After: nil,
},
},
}
err = updateUsage(txn, changes)
require.Error(t, err)
require.Contains(t, err.Error(), "negative count")
}
func TestStateStore_Usage_ServiceUsage_updatingServiceName(t *testing.T) {
s := testStateStore(t)
testRegisterNode(t, s, 1, "node1")
testRegisterService(t, s, 1, "node1", "service1")
t.Run("rename service with a single instance", func(t *testing.T) {
svc := &structs.NodeService{
ID: "service1",
Service: "after",
Address: "1.1.1.1",
Port: 1111,
}
require.NoError(t, s.EnsureService(2, "node1", svc))
// We renamed a service with a single instance, so we maintain 1 service.
idx, usage, err := s.ServiceUsage()
require.NoError(t, err)
require.Equal(t, idx, uint64(2))
require.Equal(t, usage.Services, 1)
require.Equal(t, usage.ServiceInstances, 1)
})
t.Run("rename service with a multiple instances", func(t *testing.T) {
svc2 := &structs.NodeService{
ID: "service2",
Service: "before",
Address: "1.1.1.2",
Port: 1111,
}
require.NoError(t, s.EnsureService(3, "node1", svc2))
svc3 := &structs.NodeService{
ID: "service3",
Service: "before",
Address: "1.1.1.3",
Port: 1111,
}
require.NoError(t, s.EnsureService(4, "node1", svc3))
idx, usage, err := s.ServiceUsage()
require.NoError(t, err)
require.Equal(t, idx, uint64(4))
require.Equal(t, usage.Services, 2)
require.Equal(t, usage.ServiceInstances, 3)
update := &structs.NodeService{
ID: "service2",
Service: "another-name",
Address: "1.1.1.2",
Port: 1111,
}
require.NoError(t, s.EnsureService(5, "node1", update))
idx, usage, err = s.ServiceUsage()
require.NoError(t, err)
require.Equal(t, idx, uint64(5))
require.Equal(t, usage.Services, 3)
require.Equal(t, usage.ServiceInstances, 3)
})
}

View File

@ -61,7 +61,11 @@ type changeEvents struct {
// SnapshotHandlers is a mapping of Topic to a function which produces a snapshot // SnapshotHandlers is a mapping of Topic to a function which produces a snapshot
// of events for the SubscribeRequest. Events are appended to the snapshot using SnapshotAppender. // of events for the SubscribeRequest. Events are appended to the snapshot using SnapshotAppender.
// The nil Topic is reserved and should not be used. // The nil Topic is reserved and should not be used.
type SnapshotHandlers map[Topic]func(*SubscribeRequest, SnapshotAppender) (index uint64, err error) type SnapshotHandlers map[Topic]SnapshotFunc
// SnapshotFunc builds a snapshot for the subscription request, and appends the
// events to the Snapshot using SnapshotAppender.
type SnapshotFunc func(SubscribeRequest, SnapshotAppender) (index uint64, err error)
// SnapshotAppender appends groups of events to create a Snapshot of state. // SnapshotAppender appends groups of events to create a Snapshot of state.
type SnapshotAppender interface { type SnapshotAppender interface {

View File

@ -58,7 +58,7 @@ func TestEventPublisher_PublishChangesAndSubscribe_WithSnapshot(t *testing.T) {
func newTestSnapshotHandlers() SnapshotHandlers { func newTestSnapshotHandlers() SnapshotHandlers {
return SnapshotHandlers{ return SnapshotHandlers{
testTopic: func(req *SubscribeRequest, buf SnapshotAppender) (uint64, error) { testTopic: func(req SubscribeRequest, buf SnapshotAppender) (uint64, error) {
if req.Topic != testTopic { if req.Topic != testTopic {
return 0, fmt.Errorf("unexpected topic: %v", req.Topic) return 0, fmt.Errorf("unexpected topic: %v", req.Topic)
} }
@ -117,7 +117,7 @@ func TestEventPublisher_ShutdownClosesSubscriptions(t *testing.T) {
t.Cleanup(cancel) t.Cleanup(cancel)
handlers := newTestSnapshotHandlers() handlers := newTestSnapshotHandlers()
fn := func(req *SubscribeRequest, buf SnapshotAppender) (uint64, error) { fn := func(req SubscribeRequest, buf SnapshotAppender) (uint64, error) {
return 0, nil return 0, nil
} }
handlers[intTopic(22)] = fn handlers[intTopic(22)] = fn

View File

@ -18,8 +18,6 @@ type eventSnapshot struct {
snapBuffer *eventBuffer snapBuffer *eventBuffer
} }
type snapFunc func(req *SubscribeRequest, buf SnapshotAppender) (uint64, error)
// newEventSnapshot creates a snapshot buffer based on the subscription request. // newEventSnapshot creates a snapshot buffer based on the subscription request.
// The current buffer head for the topic requested is passed so that once the // The current buffer head for the topic requested is passed so that once the
// snapshot is complete and has been delivered into the buffer, any events // snapshot is complete and has been delivered into the buffer, any events
@ -27,7 +25,7 @@ type snapFunc func(req *SubscribeRequest, buf SnapshotAppender) (uint64, error)
// missed. Once the snapshot is delivered the topic buffer is spliced onto the // missed. Once the snapshot is delivered the topic buffer is spliced onto the
// snapshot buffer so that subscribers will naturally follow from the snapshot // snapshot buffer so that subscribers will naturally follow from the snapshot
// to wait for any subsequent updates. // to wait for any subsequent updates.
func newEventSnapshot(req *SubscribeRequest, topicBufferHead *bufferItem, fn snapFunc) *eventSnapshot { func newEventSnapshot(req *SubscribeRequest, topicBufferHead *bufferItem, fn SnapshotFunc) *eventSnapshot {
buf := newEventBuffer() buf := newEventBuffer()
s := &eventSnapshot{ s := &eventSnapshot{
Head: buf.Head(), Head: buf.Head(),
@ -35,7 +33,7 @@ func newEventSnapshot(req *SubscribeRequest, topicBufferHead *bufferItem, fn sna
} }
go func() { go func() {
idx, err := fn(req, s.snapBuffer) idx, err := fn(*req, s.snapBuffer)
if err != nil { if err != nil {
s.snapBuffer.AppendItem(&bufferItem{Err: err}) s.snapBuffer.AppendItem(&bufferItem{Err: err})
return return

View File

@ -161,8 +161,8 @@ func genSequentialIDs(start, end int) []string {
return ids return ids
} }
func testHealthConsecutiveSnapshotFn(size int, index uint64) snapFunc { func testHealthConsecutiveSnapshotFn(size int, index uint64) SnapshotFunc {
return func(req *SubscribeRequest, buf SnapshotAppender) (uint64, error) { return func(req SubscribeRequest, buf SnapshotAppender) (uint64, error) {
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
// Event content is arbitrary we are just using Health because it's the // Event content is arbitrary we are just using Health because it's the
// first type defined. We just want a set of things with consecutive // first type defined. We just want a set of things with consecutive

View File

@ -0,0 +1,135 @@
package usagemetrics
import (
"context"
"errors"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/logging"
"github.com/hashicorp/go-hclog"
)
// Config holds the settings for various parameters for the
// UsageMetricsReporter
type Config struct {
logger hclog.Logger
metricLabels []metrics.Label
stateProvider StateProvider
tickerInterval time.Duration
}
// WithDatacenter adds the datacenter as a label to all metrics emitted by the
// UsageMetricsReporter
func (c *Config) WithDatacenter(dc string) *Config {
c.metricLabels = append(c.metricLabels, metrics.Label{Name: "datacenter", Value: dc})
return c
}
// WithLogger takes a logger and creates a new, named sub-logger to use when
// running
func (c *Config) WithLogger(logger hclog.Logger) *Config {
c.logger = logger.Named(logging.UsageMetrics)
return c
}
// WithReportingInterval specifies the interval on which UsageMetricsReporter
// should emit metrics
func (c *Config) WithReportingInterval(dur time.Duration) *Config {
c.tickerInterval = dur
return c
}
func (c *Config) WithStateProvider(sp StateProvider) *Config {
c.stateProvider = sp
return c
}
// StateProvider defines an inteface for retrieving a state.Store handle. In
// non-test code, this is satisfied by the fsm.FSM struct.
type StateProvider interface {
State() *state.Store
}
// UsageMetricsReporter provides functionality for emitting usage metrics into
// the metrics stream. This makes it essentially a translation layer
// between the state store and metrics stream.
type UsageMetricsReporter struct {
logger hclog.Logger
metricLabels []metrics.Label
stateProvider StateProvider
tickerInterval time.Duration
}
func NewUsageMetricsReporter(cfg *Config) (*UsageMetricsReporter, error) {
if cfg.stateProvider == nil {
return nil, errors.New("must provide a StateProvider to usage reporter")
}
if cfg.logger == nil {
cfg.logger = hclog.NewNullLogger()
}
if cfg.tickerInterval == 0 {
// Metrics are aggregated every 10 seconds, so we default to that.
cfg.tickerInterval = 10 * time.Second
}
u := &UsageMetricsReporter{
logger: cfg.logger,
stateProvider: cfg.stateProvider,
metricLabels: cfg.metricLabels,
tickerInterval: cfg.tickerInterval,
}
return u, nil
}
// Run must be run in a goroutine, and can be stopped by closing or sending
// data to the passed in shutdownCh
func (u *UsageMetricsReporter) Run(ctx context.Context) {
ticker := time.NewTicker(u.tickerInterval)
for {
select {
case <-ctx.Done():
u.logger.Debug("usage metrics reporter shutting down")
ticker.Stop()
return
case <-ticker.C:
u.runOnce()
}
}
}
func (u *UsageMetricsReporter) runOnce() {
state := u.stateProvider.State()
_, nodes, err := state.NodeCount()
if err != nil {
u.logger.Warn("failed to retrieve nodes from state store", "error", err)
}
metrics.SetGaugeWithLabels(
[]string{"consul", "state", "nodes"},
float32(nodes),
u.metricLabels,
)
_, serviceUsage, err := state.ServiceUsage()
if err != nil {
u.logger.Warn("failed to retrieve services from state store", "error", err)
}
metrics.SetGaugeWithLabels(
[]string{"consul", "state", "services"},
float32(serviceUsage.Services),
u.metricLabels,
)
metrics.SetGaugeWithLabels(
[]string{"consul", "state", "service_instances"},
float32(serviceUsage.ServiceInstances),
u.metricLabels,
)
u.emitEnterpriseUsage(serviceUsage)
}

View File

@ -0,0 +1,7 @@
// +build !consulent
package usagemetrics
import "github.com/hashicorp/consul/agent/consul/state"
func (u *UsageMetricsReporter) emitEnterpriseUsage(state.ServiceUsage) {}

View File

@ -0,0 +1,9 @@
// +build !consulent
package usagemetrics
import "github.com/hashicorp/consul/agent/consul/state"
func newStateStore() (*state.Store, error) {
return state.NewStateStore(nil)
}

View File

@ -0,0 +1,128 @@
package usagemetrics
import (
"testing"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
type mockStateProvider struct {
mock.Mock
}
func (m *mockStateProvider) State() *state.Store {
retValues := m.Called()
return retValues.Get(0).(*state.Store)
}
func TestUsageReporter_Run(t *testing.T) {
type testCase struct {
modfiyStateStore func(t *testing.T, s *state.Store)
expectedGauges map[string]metrics.GaugeValue
}
cases := map[string]testCase{
"empty-state": {
expectedGauges: map[string]metrics.GaugeValue{
"consul.usage.test.consul.state.nodes;datacenter=dc1": {
Name: "consul.usage.test.consul.state.nodes",
Value: 0,
Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}},
},
"consul.usage.test.consul.state.services;datacenter=dc1": {
Name: "consul.usage.test.consul.state.services",
Value: 0,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
},
},
"consul.usage.test.consul.state.service_instances;datacenter=dc1": {
Name: "consul.usage.test.consul.state.service_instances",
Value: 0,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
},
},
},
},
"nodes-and-services": {
modfiyStateStore: func(t *testing.T, s *state.Store) {
require.Nil(t, s.EnsureNode(1, &structs.Node{Node: "foo", Address: "127.0.0.1"}))
require.Nil(t, s.EnsureNode(2, &structs.Node{Node: "bar", Address: "127.0.0.2"}))
require.Nil(t, s.EnsureNode(3, &structs.Node{Node: "baz", Address: "127.0.0.2"}))
// Typical services and some consul services spread across two nodes
require.Nil(t, s.EnsureService(4, "foo", &structs.NodeService{ID: "db", Service: "db", Tags: nil, Address: "", Port: 5000}))
require.Nil(t, s.EnsureService(5, "bar", &structs.NodeService{ID: "api", Service: "api", Tags: nil, Address: "", Port: 5000}))
require.Nil(t, s.EnsureService(6, "foo", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil}))
require.Nil(t, s.EnsureService(7, "bar", &structs.NodeService{ID: "consul", Service: "consul", Tags: nil}))
},
expectedGauges: map[string]metrics.GaugeValue{
"consul.usage.test.consul.state.nodes;datacenter=dc1": {
Name: "consul.usage.test.consul.state.nodes",
Value: 3,
Labels: []metrics.Label{{Name: "datacenter", Value: "dc1"}},
},
"consul.usage.test.consul.state.services;datacenter=dc1": {
Name: "consul.usage.test.consul.state.services",
Value: 3,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
},
},
"consul.usage.test.consul.state.service_instances;datacenter=dc1": {
Name: "consul.usage.test.consul.state.service_instances",
Value: 4,
Labels: []metrics.Label{
{Name: "datacenter", Value: "dc1"},
},
},
},
},
}
for name, tcase := range cases {
t.Run(name, func(t *testing.T) {
// Only have a single interval for the test
sink := metrics.NewInmemSink(1*time.Minute, 1*time.Minute)
cfg := metrics.DefaultConfig("consul.usage.test")
cfg.EnableHostname = false
metrics.NewGlobal(cfg, sink)
mockStateProvider := &mockStateProvider{}
s, err := newStateStore()
require.NoError(t, err)
if tcase.modfiyStateStore != nil {
tcase.modfiyStateStore(t, s)
}
mockStateProvider.On("State").Return(s)
reporter, err := NewUsageMetricsReporter(
new(Config).
WithStateProvider(mockStateProvider).
WithLogger(testutil.Logger(t)).
WithDatacenter("dc1"),
)
require.NoError(t, err)
reporter.runOnce()
intervals := sink.Data()
require.Len(t, intervals, 1)
intv := intervals[0]
// Range over the expected values instead of just doing an Equal
// comparison on the maps because of different metrics emitted between
// OSS and Ent. The enterprise tests have a full equality comparison on
// the maps.
for key, expected := range tcase.expectedGauges {
require.Equal(t, expected, intv.Gauges[key])
}
})
}
}

View File

@ -80,16 +80,14 @@ func (e ForbiddenError) Error() string {
} }
// HTTPServer provides an HTTP api for an agent. // HTTPServer provides an HTTP api for an agent.
//
// TODO: rename this struct to something more appropriate. It is an http.Handler,
// request router or multiplexer, but it is not a Server.
type HTTPServer struct { type HTTPServer struct {
// TODO(dnephin): remove Server field, it is not used by any of the HTTPServer methods
Server *http.Server
ln net.Listener
agent *Agent agent *Agent
denylist *Denylist denylist *Denylist
// proto is filled by the agent to "http" or "https".
proto string
} }
type templatedFile struct { type templatedFile struct {
templated *bytes.Reader templated *bytes.Reader
name string name string

View File

@ -1353,7 +1353,7 @@ func TestHTTPServer_HandshakeTimeout(t *testing.T) {
// Connect to it with a plain TCP client that doesn't attempt to send HTTP or // Connect to it with a plain TCP client that doesn't attempt to send HTTP or
// complete a TLS handshake. // complete a TLS handshake.
conn, err := net.Dial("tcp", a.srv.ln.Addr().String()) conn, err := net.Dial("tcp", a.HTTPAddr())
require.NoError(t, err) require.NoError(t, err)
defer conn.Close() defer conn.Close()
@ -1413,7 +1413,7 @@ func TestRPC_HTTPSMaxConnsPerClient(t *testing.T) {
}) })
defer a.Shutdown() defer a.Shutdown()
addr := a.srv.ln.Addr() addr := a.HTTPAddr()
assertConn := func(conn net.Conn, wantOpen bool) { assertConn := func(conn net.Conn, wantOpen bool) {
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
@ -1433,21 +1433,21 @@ func TestRPC_HTTPSMaxConnsPerClient(t *testing.T) {
} }
// Connect to the server with bare TCP // Connect to the server with bare TCP
conn1, err := net.DialTimeout("tcp", addr.String(), time.Second) conn1, err := net.DialTimeout("tcp", addr, time.Second)
require.NoError(t, err) require.NoError(t, err)
defer conn1.Close() defer conn1.Close()
assertConn(conn1, true) assertConn(conn1, true)
// Two conns should succeed // Two conns should succeed
conn2, err := net.DialTimeout("tcp", addr.String(), time.Second) conn2, err := net.DialTimeout("tcp", addr, time.Second)
require.NoError(t, err) require.NoError(t, err)
defer conn2.Close() defer conn2.Close()
assertConn(conn2, true) assertConn(conn2, true)
// Third should succeed negotiating TCP handshake... // Third should succeed negotiating TCP handshake...
conn3, err := net.DialTimeout("tcp", addr.String(), time.Second) conn3, err := net.DialTimeout("tcp", addr, time.Second)
require.NoError(t, err) require.NoError(t, err)
defer conn3.Close() defer conn3.Close()
@ -1460,7 +1460,7 @@ func TestRPC_HTTPSMaxConnsPerClient(t *testing.T) {
require.NoError(t, a.reloadConfigInternal(&newCfg)) require.NoError(t, a.reloadConfigInternal(&newCfg))
// Now another conn should be allowed // Now another conn should be allowed
conn4, err := net.DialTimeout("tcp", addr.String(), time.Second) conn4, err := net.DialTimeout("tcp", addr, time.Second)
require.NoError(t, err) require.NoError(t, err)
defer conn4.Close() defer conn4.Close()

View File

@ -1,7 +1,6 @@
package agent package agent
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -10,11 +9,9 @@ import (
autoconf "github.com/hashicorp/consul/agent/auto-config" autoconf "github.com/hashicorp/consul/agent/auto-config"
"github.com/hashicorp/consul/agent/cache" "github.com/hashicorp/consul/agent/cache"
certmon "github.com/hashicorp/consul/agent/cert-monitor"
"github.com/hashicorp/consul/agent/config" "github.com/hashicorp/consul/agent/config"
"github.com/hashicorp/consul/agent/pool" "github.com/hashicorp/consul/agent/pool"
"github.com/hashicorp/consul/agent/router" "github.com/hashicorp/consul/agent/router"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/agent/token" "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
@ -82,42 +79,26 @@ func NewBaseDeps(configLoader ConfigLoader, logOut io.Writer) (BaseDeps, error)
d.RuntimeConfig = cfg d.RuntimeConfig = cfg
d.Tokens = new(token.Store) d.Tokens = new(token.Store)
// cache-types are not registered yet, but they won't be used until the components are started. // cache-types are not registered yet, but they won't be used until the components are started.
d.Cache = cache.New(cfg.Cache) d.Cache = cache.New(cfg.Cache)
d.ConnPool = newConnPool(cfg, d.Logger, d.TLSConfigurator) d.ConnPool = newConnPool(cfg, d.Logger, d.TLSConfigurator)
deferredAC := &deferredAutoConfig{}
d.Router = router.NewRouter(d.Logger, cfg.Datacenter, fmt.Sprintf("%s.%s", cfg.NodeName, cfg.Datacenter)) d.Router = router.NewRouter(d.Logger, cfg.Datacenter, fmt.Sprintf("%s.%s", cfg.NodeName, cfg.Datacenter))
cmConf := new(certmon.Config).
WithCache(d.Cache).
WithTLSConfigurator(d.TLSConfigurator).
WithDNSSANs(cfg.AutoConfig.DNSSANs).
WithIPSANs(cfg.AutoConfig.IPSANs).
WithDatacenter(cfg.Datacenter).
WithNodeName(cfg.NodeName).
WithFallback(deferredAC.autoConfigFallbackTLS).
WithLogger(d.Logger.Named(logging.AutoConfig)).
WithTokens(d.Tokens).
WithPersistence(deferredAC.autoConfigPersist)
acCertMon, err := certmon.New(cmConf)
if err != nil {
return d, err
}
acConf := autoconf.Config{ acConf := autoconf.Config{
DirectRPC: d.ConnPool, DirectRPC: d.ConnPool,
Logger: d.Logger, Logger: d.Logger,
CertMonitor: acCertMon, Loader: configLoader,
Loader: configLoader, ServerProvider: d.Router,
TLSConfigurator: d.TLSConfigurator,
Cache: d.Cache,
Tokens: d.Tokens,
} }
d.AutoConfig, err = autoconf.New(acConf) d.AutoConfig, err = autoconf.New(acConf)
if err != nil { if err != nil {
return d, err return d, err
} }
// TODO: can this cyclic dependency be un-cycled?
deferredAC.autoConf = d.AutoConfig
return d, nil return d, nil
} }
@ -144,21 +125,3 @@ func newConnPool(config *config.RuntimeConfig, logger hclog.Logger, tls *tlsutil
} }
return pool return pool
} }
type deferredAutoConfig struct {
autoConf *autoconf.AutoConfig // TODO: use an interface
}
func (a *deferredAutoConfig) autoConfigFallbackTLS(ctx context.Context) (*structs.SignedResponse, error) {
if a.autoConf == nil {
return nil, fmt.Errorf("AutoConfig manager has not been created yet")
}
return a.autoConf.FallbackTLS(ctx)
}
func (a *deferredAutoConfig) autoConfigPersist(resp *structs.SignedResponse) error {
if a.autoConf == nil {
return fmt.Errorf("AutoConfig manager has not been created yet")
}
return a.autoConf.RecordUpdatedCerts(resp)
}

View File

@ -10,6 +10,7 @@ import (
"github.com/hashicorp/consul/lib" "github.com/hashicorp/consul/lib"
"github.com/hashicorp/consul/lib/decode" "github.com/hashicorp/consul/lib/decode"
"github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/go-msgpack/codec"
"github.com/hashicorp/go-multierror"
"github.com/mitchellh/hashstructure" "github.com/mitchellh/hashstructure"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
) )
@ -43,6 +44,7 @@ type ConfigEntry interface {
CanRead(acl.Authorizer) bool CanRead(acl.Authorizer) bool
CanWrite(acl.Authorizer) bool CanWrite(acl.Authorizer) bool
GetMeta() map[string]string
GetEnterpriseMeta() *EnterpriseMeta GetEnterpriseMeta() *EnterpriseMeta
GetRaftIndex() *RaftIndex GetRaftIndex() *RaftIndex
} }
@ -64,6 +66,7 @@ type ServiceConfigEntry struct {
// //
// Connect ConnectConfiguration // Connect ConnectConfiguration
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -80,6 +83,13 @@ func (e *ServiceConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *ServiceConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *ServiceConfigEntry) Normalize() error { func (e *ServiceConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -94,7 +104,7 @@ func (e *ServiceConfigEntry) Normalize() error {
} }
func (e *ServiceConfigEntry) Validate() error { func (e *ServiceConfigEntry) Validate() error {
return nil return validateConfigEntryMeta(e.Meta)
} }
func (e *ServiceConfigEntry) CanRead(authz acl.Authorizer) bool { func (e *ServiceConfigEntry) CanRead(authz acl.Authorizer) bool {
@ -137,6 +147,7 @@ type ProxyConfigEntry struct {
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -153,6 +164,13 @@ func (e *ProxyConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *ProxyConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *ProxyConfigEntry) Normalize() error { func (e *ProxyConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -175,6 +193,10 @@ func (e *ProxyConfigEntry) Validate() error {
return fmt.Errorf("invalid name (%q), only %q is supported", e.Name, ProxyConfigGlobal) return fmt.Errorf("invalid name (%q), only %q is supported", e.Name, ProxyConfigGlobal)
} }
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
return e.validateEnterpriseMeta() return e.validateEnterpriseMeta()
} }
@ -666,4 +688,38 @@ func (c *ConfigEntryResponse) UnmarshalBinary(data []byte) error {
type ConfigEntryKindName struct { type ConfigEntryKindName struct {
Kind string Kind string
Name string Name string
EnterpriseMeta
}
func NewConfigEntryKindName(kind, name string, entMeta *EnterpriseMeta) ConfigEntryKindName {
ret := ConfigEntryKindName{
Kind: kind,
Name: name,
}
if entMeta == nil {
entMeta = DefaultEnterpriseMeta()
}
ret.EnterpriseMeta = *entMeta
ret.EnterpriseMeta.Normalize()
return ret
}
func validateConfigEntryMeta(meta map[string]string) error {
var err error
if len(meta) > metaMaxKeyPairs {
err = multierror.Append(err, fmt.Errorf(
"Meta exceeds maximum element count %d", metaMaxKeyPairs))
}
for k, v := range meta {
if len(k) > metaKeyMaxLength {
err = multierror.Append(err, fmt.Errorf(
"Meta key %q exceeds maximum length %d", k, metaKeyMaxLength))
}
if len(v) > metaValueMaxLength {
err = multierror.Append(err, fmt.Errorf(
"Meta value for key %q exceeds maximum length %d", k, metaValueMaxLength))
}
}
return err
} }

View File

@ -69,6 +69,7 @@ type ServiceRouterConfigEntry struct {
// the default service. // the default service.
Routes []ServiceRoute Routes []ServiceRoute
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -85,6 +86,13 @@ func (e *ServiceRouterConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *ServiceRouterConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *ServiceRouterConfigEntry) Normalize() error { func (e *ServiceRouterConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -120,6 +128,10 @@ func (e *ServiceRouterConfigEntry) Validate() error {
return fmt.Errorf("Name is required") return fmt.Errorf("Name is required")
} }
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
// Technically you can have no explicit routes at all where just the // Technically you can have no explicit routes at all where just the
// catch-all is configured for you, but at that point maybe you should just // catch-all is configured for you, but at that point maybe you should just
// delete it so it will default? // delete it so it will default?
@ -438,6 +450,7 @@ type ServiceSplitterConfigEntry struct {
// to the FIRST split. // to the FIRST split.
Splits []ServiceSplit Splits []ServiceSplit
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -454,6 +467,13 @@ func (e *ServiceSplitterConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *ServiceSplitterConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *ServiceSplitterConfigEntry) Normalize() error { func (e *ServiceSplitterConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -492,6 +512,10 @@ func (e *ServiceSplitterConfigEntry) Validate() error {
return fmt.Errorf("no splits configured") return fmt.Errorf("no splits configured")
} }
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
const maxScaledWeight = 100 * 100 const maxScaledWeight = 100 * 100
copyAsKey := func(s ServiceSplit) ServiceSplit { copyAsKey := func(s ServiceSplit) ServiceSplit {
@ -674,6 +698,7 @@ type ServiceResolverConfigEntry struct {
// issuing requests to this upstream service. // issuing requests to this upstream service.
LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"` LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"`
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -746,6 +771,13 @@ func (e *ServiceResolverConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *ServiceResolverConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *ServiceResolverConfigEntry) Normalize() error { func (e *ServiceResolverConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -763,6 +795,10 @@ func (e *ServiceResolverConfigEntry) Validate() error {
return fmt.Errorf("Name is required") return fmt.Errorf("Name is required")
} }
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
if len(e.Subsets) > 0 { if len(e.Subsets) > 0 {
for name := range e.Subsets { for name := range e.Subsets {
if name == "" { if name == "" {

View File

@ -27,6 +27,7 @@ type IngressGatewayConfigEntry struct {
// what services to associated to those ports. // what services to associated to those ports.
Listeners []IngressListener Listeners []IngressListener
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -73,6 +74,7 @@ type IngressService struct {
// using a "tcp" listener. // using a "tcp" listener.
Hosts []string Hosts []string
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
} }
@ -93,6 +95,13 @@ func (e *IngressGatewayConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *IngressGatewayConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *IngressGatewayConfigEntry) Normalize() error { func (e *IngressGatewayConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -121,6 +130,10 @@ func (e *IngressGatewayConfigEntry) Normalize() error {
} }
func (e *IngressGatewayConfigEntry) Validate() error { func (e *IngressGatewayConfigEntry) Validate() error {
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
validProtocols := map[string]bool{ validProtocols := map[string]bool{
"tcp": true, "tcp": true,
"http": true, "http": true,
@ -283,6 +296,7 @@ type TerminatingGatewayConfigEntry struct {
Name string Name string
Services []LinkedService Services []LinkedService
Meta map[string]string `json:",omitempty"`
EnterpriseMeta `hcl:",squash" mapstructure:",squash"` EnterpriseMeta `hcl:",squash" mapstructure:",squash"`
RaftIndex RaftIndex
} }
@ -322,6 +336,13 @@ func (e *TerminatingGatewayConfigEntry) GetName() string {
return e.Name return e.Name
} }
func (e *TerminatingGatewayConfigEntry) GetMeta() map[string]string {
if e == nil {
return nil
}
return e.Meta
}
func (e *TerminatingGatewayConfigEntry) Normalize() error { func (e *TerminatingGatewayConfigEntry) Normalize() error {
if e == nil { if e == nil {
return fmt.Errorf("config entry is nil") return fmt.Errorf("config entry is nil")
@ -339,6 +360,10 @@ func (e *TerminatingGatewayConfigEntry) Normalize() error {
} }
func (e *TerminatingGatewayConfigEntry) Validate() error { func (e *TerminatingGatewayConfigEntry) Validate() error {
if err := validateConfigEntryMeta(e.Meta); err != nil {
return err
}
seen := make(map[ServiceID]bool) seen := make(map[ServiceID]bool)
for _, svc := range e.Services { for _, svc := range e.Services {

View File

@ -46,6 +46,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "proxy-defaults" kind = "proxy-defaults"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
config { config {
"foo" = 19 "foo" = 19
"bar" = "abc" "bar" = "abc"
@ -60,6 +64,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "proxy-defaults" Kind = "proxy-defaults"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Config { Config {
"foo" = 19 "foo" = 19
"bar" = "abc" "bar" = "abc"
@ -74,6 +82,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &ProxyConfigEntry{ expect: &ProxyConfigEntry{
Kind: "proxy-defaults", Kind: "proxy-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Config: map[string]interface{}{ Config: map[string]interface{}{
"foo": 19, "foo": 19,
"bar": "abc", "bar": "abc",
@ -91,6 +103,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-defaults" kind = "service-defaults"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
protocol = "http" protocol = "http"
external_sni = "abc-123" external_sni = "abc-123"
mesh_gateway { mesh_gateway {
@ -100,6 +116,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-defaults" Kind = "service-defaults"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Protocol = "http" Protocol = "http"
ExternalSNI = "abc-123" ExternalSNI = "abc-123"
MeshGateway { MeshGateway {
@ -107,8 +127,12 @@ func TestDecodeConfigEntry(t *testing.T) {
} }
`, `,
expect: &ServiceConfigEntry{ expect: &ServiceConfigEntry{
Kind: "service-defaults", Kind: "service-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Protocol: "http", Protocol: "http",
ExternalSNI: "abc-123", ExternalSNI: "abc-123",
MeshGateway: MeshGatewayConfig{ MeshGateway: MeshGatewayConfig{
@ -121,6 +145,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-router" kind = "service-router"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
routes = [ routes = [
{ {
match { match {
@ -200,6 +228,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-router" Kind = "service-router"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Routes = [ Routes = [
{ {
Match { Match {
@ -279,6 +311,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &ServiceRouterConfigEntry{ expect: &ServiceRouterConfigEntry{
Kind: "service-router", Kind: "service-router",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Routes: []ServiceRoute{ Routes: []ServiceRoute{
{ {
Match: &ServiceRouteMatch{ Match: &ServiceRouteMatch{
@ -361,6 +397,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-splitter" kind = "service-splitter"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
splits = [ splits = [
{ {
weight = 99.1 weight = 99.1
@ -376,6 +416,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-splitter" Kind = "service-splitter"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Splits = [ Splits = [
{ {
Weight = 99.1 Weight = 99.1
@ -391,6 +435,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &ServiceSplitterConfigEntry{ expect: &ServiceSplitterConfigEntry{
Kind: ServiceSplitter, Kind: ServiceSplitter,
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Splits: []ServiceSplit{ Splits: []ServiceSplit{
{ {
Weight: 99.1, Weight: 99.1,
@ -409,6 +457,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-resolver" kind = "service-resolver"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
default_subset = "v1" default_subset = "v1"
connect_timeout = "15s" connect_timeout = "15s"
subsets = { subsets = {
@ -434,6 +486,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-resolver" Kind = "service-resolver"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
DefaultSubset = "v1" DefaultSubset = "v1"
ConnectTimeout = "15s" ConnectTimeout = "15s"
Subsets = { Subsets = {
@ -457,8 +513,12 @@ func TestDecodeConfigEntry(t *testing.T) {
} }
}`, }`,
expect: &ServiceResolverConfigEntry{ expect: &ServiceResolverConfigEntry{
Kind: "service-resolver", Kind: "service-resolver",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
DefaultSubset: "v1", DefaultSubset: "v1",
ConnectTimeout: 15 * time.Second, ConnectTimeout: 15 * time.Second,
Subsets: map[string]ServiceResolverSubset{ Subsets: map[string]ServiceResolverSubset{
@ -686,6 +746,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "ingress-gateway" kind = "ingress-gateway"
name = "ingress-web" name = "ingress-web"
meta {
"foo" = "bar"
"gir" = "zim"
}
tls { tls {
enabled = true enabled = true
@ -728,6 +792,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "ingress-gateway" Kind = "ingress-gateway"
Name = "ingress-web" Name = "ingress-web"
Meta {
"foo" = "bar"
"gir" = "zim"
}
TLS { TLS {
Enabled = true Enabled = true
} }
@ -768,6 +836,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &IngressGatewayConfigEntry{ expect: &IngressGatewayConfigEntry{
Kind: "ingress-gateway", Kind: "ingress-gateway",
Name: "ingress-web", Name: "ingress-web",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
TLS: GatewayTLSConfig{ TLS: GatewayTLSConfig{
Enabled: true, Enabled: true,
}, },
@ -811,6 +883,10 @@ func TestDecodeConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "terminating-gateway" kind = "terminating-gateway"
name = "terminating-gw-west" name = "terminating-gw-west"
meta {
"foo" = "bar"
"gir" = "zim"
}
services = [ services = [
{ {
name = "payments", name = "payments",
@ -831,6 +907,10 @@ func TestDecodeConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "terminating-gateway" Kind = "terminating-gateway"
Name = "terminating-gw-west" Name = "terminating-gw-west"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Services = [ Services = [
{ {
Name = "payments", Name = "payments",
@ -851,6 +931,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &TerminatingGatewayConfigEntry{ expect: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway", Kind: "terminating-gateway",
Name: "terminating-gw-west", Name: "terminating-gw-west",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Services: []LinkedService{ Services: []LinkedService{
{ {
Name: "payments", Name: "payments",

View File

@ -73,8 +73,7 @@ type TestAgent struct {
// It is valid after Start(). // It is valid after Start().
dns *DNSServer dns *DNSServer
// srv is a reference to the first started HTTP endpoint. // srv is an HTTPServer that may be used to test http endpoints.
// It is valid after Start().
srv *HTTPServer srv *HTTPServer
// overrides is an hcl config source to use to override otherwise // overrides is an hcl config source to use to override otherwise
@ -213,6 +212,8 @@ func (a *TestAgent) Start(t *testing.T) (err error) {
// Start the anti-entropy syncer // Start the anti-entropy syncer
a.Agent.StartSync() a.Agent.StartSync()
a.srv = &HTTPServer{agent: agent, denylist: NewDenylist(a.config.HTTPBlockEndpoints)}
if err := a.waitForUp(); err != nil { if err := a.waitForUp(); err != nil {
a.Shutdown() a.Shutdown()
t.Logf("Error while waiting for test agent to start: %v", err) t.Logf("Error while waiting for test agent to start: %v", err)
@ -220,7 +221,6 @@ func (a *TestAgent) Start(t *testing.T) (err error) {
} }
a.dns = a.dnsServers[0] a.dns = a.dnsServers[0]
a.srv = a.httpServers[0]
return nil return nil
} }
@ -233,7 +233,7 @@ func (a *TestAgent) waitForUp() error {
var retErr error var retErr error
var out structs.IndexedNodes var out structs.IndexedNodes
for ; !time.Now().After(deadline); time.Sleep(timer.Wait) { for ; !time.Now().After(deadline); time.Sleep(timer.Wait) {
if len(a.httpServers) == 0 { if len(a.apiServers.servers) == 0 {
retErr = fmt.Errorf("waiting for server") retErr = fmt.Errorf("waiting for server")
continue // fail, try again continue // fail, try again
} }
@ -262,7 +262,7 @@ func (a *TestAgent) waitForUp() error {
} else { } else {
req := httptest.NewRequest("GET", "/v1/agent/self", nil) req := httptest.NewRequest("GET", "/v1/agent/self", nil)
resp := httptest.NewRecorder() resp := httptest.NewRecorder()
_, err := a.httpServers[0].AgentSelf(resp, req) _, err := a.srv.AgentSelf(resp, req)
if acl.IsErrPermissionDenied(err) || resp.Code == 403 { if acl.IsErrPermissionDenied(err) || resp.Code == 403 {
// permission denied is enough to show that the client is // permission denied is enough to show that the client is
// connected to the servers as it would get a 503 if // connected to the servers as it would get a 503 if
@ -313,10 +313,13 @@ func (a *TestAgent) DNSAddr() string {
} }
func (a *TestAgent) HTTPAddr() string { func (a *TestAgent) HTTPAddr() string {
if a.srv == nil { var srv apiServer
return "" for _, srv = range a.Agent.apiServers.servers {
if srv.Protocol == "http" {
break
}
} }
return a.srv.Server.Addr return srv.Addr.String()
} }
func (a *TestAgent) SegmentAddr(name string) string { func (a *TestAgent) SegmentAddr(name string) string {

192
agent/token/persistence.go Normal file
View File

@ -0,0 +1,192 @@
package token
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/consul/lib/file"
)
// Logger used by Store.Load to report warnings.
type Logger interface {
Warn(msg string, args ...interface{})
}
// Config used by Store.Load, which includes tokens and settings for persistence.
type Config struct {
EnablePersistence bool
DataDir string
ACLDefaultToken string
ACLAgentToken string
ACLAgentMasterToken string
ACLReplicationToken string
EnterpriseConfig
}
const tokensPath = "acl-tokens.json"
// Load tokens from Config and optionally from a persisted file in the cfg.DataDir.
// If a token exists in both the persisted file and in the Config a warning will
// be logged and the persisted token will be used.
//
// Failures to load the persisted file will result in loading tokens from the
// config before returning the error.
func (t *Store) Load(cfg Config, logger Logger) error {
t.persistenceLock.RLock()
if !cfg.EnablePersistence {
t.persistence = nil
t.persistenceLock.RUnlock()
loadTokens(t, cfg, persistedTokens{}, logger)
return nil
}
defer t.persistenceLock.RUnlock()
t.persistence = &fileStore{
filename: filepath.Join(cfg.DataDir, tokensPath),
logger: logger,
}
return t.persistence.load(t, cfg)
}
// WithPersistenceLock executes f while hold a lock. If f returns a nil error,
// the tokens in Store will be persisted to the tokens file. Otherwise no
// tokens will be persisted, and the error from f will be returned.
//
// The lock is held so that the writes are persisted before some other thread
// can change the value.
func (t *Store) WithPersistenceLock(f func() error) error {
t.persistenceLock.Lock()
if t.persistence == nil {
t.persistenceLock.Unlock()
return f()
}
defer t.persistenceLock.Unlock()
return t.persistence.withPersistenceLock(t, f)
}
type persistedTokens struct {
Replication string `json:"replication,omitempty"`
AgentMaster string `json:"agent_master,omitempty"`
Default string `json:"default,omitempty"`
Agent string `json:"agent,omitempty"`
}
type fileStore struct {
filename string
logger Logger
}
func (p *fileStore) load(s *Store, cfg Config) error {
tokens, err := readPersistedFromFile(p.filename)
if err != nil {
p.logger.Warn("unable to load persisted tokens", "error", err)
}
loadTokens(s, cfg, tokens, p.logger)
return err
}
func loadTokens(s *Store, cfg Config, tokens persistedTokens, logger Logger) {
if tokens.Default != "" {
s.UpdateUserToken(tokens.Default, TokenSourceAPI)
if cfg.ACLDefaultToken != "" {
logger.Warn("\"default\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateUserToken(cfg.ACLDefaultToken, TokenSourceConfig)
}
if tokens.Agent != "" {
s.UpdateAgentToken(tokens.Agent, TokenSourceAPI)
if cfg.ACLAgentToken != "" {
logger.Warn("\"agent\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateAgentToken(cfg.ACLAgentToken, TokenSourceConfig)
}
if tokens.AgentMaster != "" {
s.UpdateAgentMasterToken(tokens.AgentMaster, TokenSourceAPI)
if cfg.ACLAgentMasterToken != "" {
logger.Warn("\"agent_master\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateAgentMasterToken(cfg.ACLAgentMasterToken, TokenSourceConfig)
}
if tokens.Replication != "" {
s.UpdateReplicationToken(tokens.Replication, TokenSourceAPI)
if cfg.ACLReplicationToken != "" {
logger.Warn("\"replication\" token present in both the configuration and persisted token store, using the persisted token")
}
} else {
s.UpdateReplicationToken(cfg.ACLReplicationToken, TokenSourceConfig)
}
loadEnterpriseTokens(s, cfg)
}
func readPersistedFromFile(filename string) (persistedTokens, error) {
tokens := persistedTokens{}
buf, err := ioutil.ReadFile(filename)
switch {
case os.IsNotExist(err):
// non-existence is not an error we care about
return tokens, nil
case err != nil:
return tokens, fmt.Errorf("failed reading tokens file %q: %w", filename, err)
}
if err := json.Unmarshal(buf, &tokens); err != nil {
return tokens, fmt.Errorf("failed to decode tokens file %q: %w", filename, err)
}
return tokens, nil
}
func (p *fileStore) withPersistenceLock(s *Store, f func() error) error {
if err := f(); err != nil {
return err
}
return p.saveToFile(s)
}
func (p *fileStore) saveToFile(s *Store) error {
tokens := persistedTokens{}
if tok, source := s.UserTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.Default = tok
}
if tok, source := s.AgentTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.Agent = tok
}
if tok, source := s.AgentMasterTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.AgentMaster = tok
}
if tok, source := s.ReplicationTokenAndSource(); tok != "" && source == TokenSourceAPI {
tokens.Replication = tok
}
data, err := json.Marshal(tokens)
if err != nil {
p.logger.Warn("failed to persist tokens", "error", err)
return fmt.Errorf("Failed to marshal tokens for persistence: %v", err)
}
if err := file.WriteAtomicWithPerms(p.filename, data, 0700, 0600); err != nil {
p.logger.Warn("failed to persist tokens", "error", err)
return fmt.Errorf("Failed to persist tokens - %v", err)
}
return nil
}

View File

@ -0,0 +1,213 @@
package token
import (
"io/ioutil"
"path/filepath"
"testing"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/require"
)
func TestStore_Load(t *testing.T) {
dataDir := testutil.TempDir(t, "datadir")
tokenFile := filepath.Join(dataDir, tokensPath)
logger := hclog.New(nil)
store := new(Store)
t.Run("with empty store", func(t *testing.T) {
cfg := Config{
DataDir: dataDir,
ACLAgentToken: "alfa",
ACLAgentMasterToken: "bravo",
ACLDefaultToken: "charlie",
ACLReplicationToken: "delta",
}
require.NoError(t, store.Load(cfg, logger))
require.Equal(t, "alfa", store.AgentToken())
require.Equal(t, "bravo", store.AgentMasterToken())
require.Equal(t, "charlie", store.UserToken())
require.Equal(t, "delta", store.ReplicationToken())
})
t.Run("updated from Config", func(t *testing.T) {
cfg := Config{
DataDir: dataDir,
ACLDefaultToken: "echo",
ACLAgentToken: "foxtrot",
ACLAgentMasterToken: "golf",
ACLReplicationToken: "hotel",
}
// ensures no error for missing persisted tokens file
require.NoError(t, store.Load(cfg, logger))
require.Equal(t, "echo", store.UserToken())
require.Equal(t, "foxtrot", store.AgentToken())
require.Equal(t, "golf", store.AgentMasterToken())
require.Equal(t, "hotel", store.ReplicationToken())
})
t.Run("with persisted tokens", func(t *testing.T) {
cfg := Config{
DataDir: dataDir,
ACLDefaultToken: "echo",
ACLAgentToken: "foxtrot",
ACLAgentMasterToken: "golf",
ACLReplicationToken: "hotel",
}
tokens := `{
"agent" : "india",
"agent_master" : "juliett",
"default": "kilo",
"replication" : "lima"
}`
require.NoError(t, ioutil.WriteFile(tokenFile, []byte(tokens), 0600))
require.NoError(t, store.Load(cfg, logger))
// no updates since token persistence is not enabled
require.Equal(t, "echo", store.UserToken())
require.Equal(t, "foxtrot", store.AgentToken())
require.Equal(t, "golf", store.AgentMasterToken())
require.Equal(t, "hotel", store.ReplicationToken())
cfg.EnablePersistence = true
require.NoError(t, store.Load(cfg, logger))
require.Equal(t, "india", store.AgentToken())
require.Equal(t, "juliett", store.AgentMasterToken())
require.Equal(t, "kilo", store.UserToken())
require.Equal(t, "lima", store.ReplicationToken())
// check store persistence was enabled
require.NotNil(t, store.persistence)
})
t.Run("with persisted tokens, persisted tokens override config", func(t *testing.T) {
tokens := `{
"agent" : "mike",
"agent_master" : "november",
"default": "oscar",
"replication" : "papa"
}`
cfg := Config{
EnablePersistence: true,
DataDir: dataDir,
ACLDefaultToken: "quebec",
ACLAgentToken: "romeo",
ACLAgentMasterToken: "sierra",
ACLReplicationToken: "tango",
}
require.NoError(t, ioutil.WriteFile(tokenFile, []byte(tokens), 0600))
require.NoError(t, store.Load(cfg, logger))
require.Equal(t, "mike", store.AgentToken())
require.Equal(t, "november", store.AgentMasterToken())
require.Equal(t, "oscar", store.UserToken())
require.Equal(t, "papa", store.ReplicationToken())
})
t.Run("with some persisted tokens", func(t *testing.T) {
tokens := `{
"agent" : "uniform",
"agent_master" : "victor"
}`
cfg := Config{
EnablePersistence: true,
DataDir: dataDir,
ACLDefaultToken: "whiskey",
ACLAgentToken: "xray",
ACLAgentMasterToken: "yankee",
ACLReplicationToken: "zulu",
}
require.NoError(t, ioutil.WriteFile(tokenFile, []byte(tokens), 0600))
require.NoError(t, store.Load(cfg, logger))
require.Equal(t, "uniform", store.AgentToken())
require.Equal(t, "victor", store.AgentMasterToken())
require.Equal(t, "whiskey", store.UserToken())
require.Equal(t, "zulu", store.ReplicationToken())
})
t.Run("persisted file contains invalid data", func(t *testing.T) {
cfg := Config{
EnablePersistence: true,
DataDir: dataDir,
ACLDefaultToken: "one",
ACLAgentToken: "two",
ACLAgentMasterToken: "three",
ACLReplicationToken: "four",
}
require.NoError(t, ioutil.WriteFile(tokenFile, []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, 0600))
err := store.Load(cfg, logger)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to decode tokens file")
require.Equal(t, "one", store.UserToken())
require.Equal(t, "two", store.AgentToken())
require.Equal(t, "three", store.AgentMasterToken())
require.Equal(t, "four", store.ReplicationToken())
})
t.Run("persisted file contains invalid json", func(t *testing.T) {
cfg := Config{
EnablePersistence: true,
DataDir: dataDir,
ACLDefaultToken: "alfa",
ACLAgentToken: "bravo",
ACLAgentMasterToken: "charlie",
ACLReplicationToken: "foxtrot",
}
require.NoError(t, ioutil.WriteFile(tokenFile, []byte("[1,2,3]"), 0600))
err := store.Load(cfg, logger)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to decode tokens file")
require.Equal(t, "alfa", store.UserToken())
require.Equal(t, "bravo", store.AgentToken())
require.Equal(t, "charlie", store.AgentMasterToken())
require.Equal(t, "foxtrot", store.ReplicationToken())
})
}
func TestStore_WithPersistenceLock(t *testing.T) {
dataDir := testutil.TempDir(t, "datadir")
store := new(Store)
cfg := Config{
EnablePersistence: true,
DataDir: dataDir,
ACLDefaultToken: "default-token",
ACLAgentToken: "agent-token",
ACLAgentMasterToken: "master-token",
ACLReplicationToken: "replication-token",
}
err := store.Load(cfg, hclog.New(nil))
require.NoError(t, err)
f := func() error {
updated := store.UpdateUserToken("the-new-token", TokenSourceAPI)
require.True(t, updated)
updated = store.UpdateAgentMasterToken("the-new-master-token", TokenSourceAPI)
require.True(t, updated)
return nil
}
err = store.WithPersistenceLock(f)
require.NoError(t, err)
tokens, err := readPersistedFromFile(filepath.Join(dataDir, tokensPath))
require.NoError(t, err)
expected := persistedTokens{
Default: "the-new-token",
AgentMaster: "the-new-master-token",
}
require.Equal(t, expected, tokens)
}

View File

@ -77,6 +77,12 @@ type Store struct {
watchers map[int]watcher watchers map[int]watcher
watcherIndex int watcherIndex int
persistence *fileStore
// persistenceLock is used to synchronize access to the persisted token store
// within the data directory. This will prevent loading while writing as well as
// multiple concurrent writes.
persistenceLock sync.RWMutex
// enterpriseTokens contains tokens only used in consul-enterprise // enterpriseTokens contains tokens only used in consul-enterprise
enterpriseTokens enterpriseTokens
} }
@ -158,7 +164,7 @@ func (t *Store) sendNotificationLocked(kinds ...TokenKind) {
// Returns true if it was changed. // Returns true if it was changed.
func (t *Store) UpdateUserToken(token string, source TokenSource) bool { func (t *Store) UpdateUserToken(token string, source TokenSource) bool {
t.l.Lock() t.l.Lock()
changed := (t.userToken != token || t.userTokenSource != source) changed := t.userToken != token || t.userTokenSource != source
t.userToken = token t.userToken = token
t.userTokenSource = source t.userTokenSource = source
if changed { if changed {
@ -172,7 +178,7 @@ func (t *Store) UpdateUserToken(token string, source TokenSource) bool {
// Returns true if it was changed. // Returns true if it was changed.
func (t *Store) UpdateAgentToken(token string, source TokenSource) bool { func (t *Store) UpdateAgentToken(token string, source TokenSource) bool {
t.l.Lock() t.l.Lock()
changed := (t.agentToken != token || t.agentTokenSource != source) changed := t.agentToken != token || t.agentTokenSource != source
t.agentToken = token t.agentToken = token
t.agentTokenSource = source t.agentTokenSource = source
if changed { if changed {
@ -186,7 +192,7 @@ func (t *Store) UpdateAgentToken(token string, source TokenSource) bool {
// Returns true if it was changed. // Returns true if it was changed.
func (t *Store) UpdateAgentMasterToken(token string, source TokenSource) bool { func (t *Store) UpdateAgentMasterToken(token string, source TokenSource) bool {
t.l.Lock() t.l.Lock()
changed := (t.agentMasterToken != token || t.agentMasterTokenSource != source) changed := t.agentMasterToken != token || t.agentMasterTokenSource != source
t.agentMasterToken = token t.agentMasterToken = token
t.agentMasterTokenSource = source t.agentMasterTokenSource = source
if changed { if changed {
@ -200,7 +206,7 @@ func (t *Store) UpdateAgentMasterToken(token string, source TokenSource) bool {
// Returns true if it was changed. // Returns true if it was changed.
func (t *Store) UpdateReplicationToken(token string, source TokenSource) bool { func (t *Store) UpdateReplicationToken(token string, source TokenSource) bool {
t.l.Lock() t.l.Lock()
changed := (t.replicationToken != token || t.replicationTokenSource != source) changed := t.replicationToken != token || t.replicationTokenSource != source
t.replicationToken = token t.replicationToken = token
t.replicationTokenSource = source t.replicationTokenSource = source
if changed { if changed {

View File

@ -2,11 +2,18 @@
package token package token
type EnterpriseConfig struct {
}
// Stub for enterpriseTokens // Stub for enterpriseTokens
type enterpriseTokens struct { type enterpriseTokens struct {
} }
// enterpriseAgentToken OSS stub // enterpriseAgentToken OSS stub
func (s *Store) enterpriseAgentToken() string { func (t *Store) enterpriseAgentToken() string {
return "" return ""
} }
// loadEnterpriseTokens is a noop stub for the func defined agent_ent.go
func loadEnterpriseTokens(_ *Store, _ Config) {
}

View File

@ -7,8 +7,6 @@ import (
) )
func TestStore_RegularTokens(t *testing.T) { func TestStore_RegularTokens(t *testing.T) {
t.Parallel()
type tokens struct { type tokens struct {
userSource TokenSource userSource TokenSource
user string user string
@ -89,13 +87,22 @@ func TestStore_RegularTokens(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
t.Parallel()
s := new(Store) s := new(Store)
require.True(t, s.UpdateUserToken(tt.set.user, tt.set.userSource)) if tt.set.user != "" {
require.True(t, s.UpdateAgentToken(tt.set.agent, tt.set.agentSource)) require.True(t, s.UpdateUserToken(tt.set.user, tt.set.userSource))
require.True(t, s.UpdateReplicationToken(tt.set.repl, tt.set.replSource)) }
require.True(t, s.UpdateAgentMasterToken(tt.set.master, tt.set.masterSource))
if tt.set.agent != "" {
require.True(t, s.UpdateAgentToken(tt.set.agent, tt.set.agentSource))
}
if tt.set.repl != "" {
require.True(t, s.UpdateReplicationToken(tt.set.repl, tt.set.replSource))
}
if tt.set.master != "" {
require.True(t, s.UpdateAgentMasterToken(tt.set.master, tt.set.masterSource))
}
// If they don't change then they return false. // If they don't change then they return false.
require.False(t, s.UpdateUserToken(tt.set.user, tt.set.userSource)) require.False(t, s.UpdateUserToken(tt.set.user, tt.set.userSource))
@ -128,7 +135,6 @@ func TestStore_RegularTokens(t *testing.T) {
} }
func TestStore_AgentMasterToken(t *testing.T) { func TestStore_AgentMasterToken(t *testing.T) {
t.Parallel()
s := new(Store) s := new(Store)
verify := func(want bool, toks ...string) { verify := func(want bool, toks ...string) {
@ -152,7 +158,6 @@ func TestStore_AgentMasterToken(t *testing.T) {
} }
func TestStore_Notify(t *testing.T) { func TestStore_Notify(t *testing.T) {
t.Parallel()
s := new(Store) s := new(Store)
newNotification := func(t *testing.T, s *Store, kind TokenKind) Notifier { newNotification := func(t *testing.T, s *Store, kind TokenKind) Notifier {

View File

@ -41,7 +41,7 @@ func TestUiIndex(t *testing.T) {
// Register node // Register node
req, _ := http.NewRequest("GET", "/ui/my-file", nil) req, _ := http.NewRequest("GET", "/ui/my-file", nil)
req.URL.Scheme = "http" req.URL.Scheme = "http"
req.URL.Host = a.srv.Server.Addr req.URL.Host = a.HTTPAddr()
// Make the request // Make the request
client := cleanhttp.DefaultClient() client := cleanhttp.DefaultClient()

View File

@ -607,9 +607,11 @@ func NewClient(config *Config) (*Client, error) {
trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { trans.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
return net.Dial("unix", parts[1]) return net.Dial("unix", parts[1])
} }
config.HttpClient = &http.Client{ httpClient, err := NewHttpClient(trans, config.TLSConfig)
Transport: trans, if err != nil {
return nil, err
} }
config.HttpClient = httpClient
default: default:
return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0]) return nil, fmt.Errorf("Unknown protocol scheme: %s", parts[0])
} }

View File

@ -95,6 +95,7 @@ type ServiceConfigEntry struct {
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"`
ExternalSNI string `json:",omitempty" alias:"external_sni"` ExternalSNI string `json:",omitempty" alias:"external_sni"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
@ -122,6 +123,7 @@ type ProxyConfigEntry struct {
Config map[string]interface{} `json:",omitempty"` Config map[string]interface{} `json:",omitempty"`
MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"` MeshGateway MeshGatewayConfig `json:",omitempty" alias:"mesh_gateway"`
Expose ExposeConfig `json:",omitempty"` Expose ExposeConfig `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }

View File

@ -12,6 +12,7 @@ type ServiceRouterConfigEntry struct {
Routes []ServiceRoute `json:",omitempty"` Routes []ServiceRoute `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
@ -111,6 +112,7 @@ type ServiceSplitterConfigEntry struct {
Splits []ServiceSplit `json:",omitempty"` Splits []ServiceSplit `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }
@ -142,6 +144,7 @@ type ServiceResolverConfigEntry struct {
// issuing requests to this upstream service. // issuing requests to this upstream service.
LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"` LoadBalancer *LoadBalancer `json:",omitempty" alias:"load_balancer"`
Meta map[string]string `json:",omitempty"`
CreateIndex uint64 CreateIndex uint64
ModifyIndex uint64 ModifyIndex uint64
} }

View File

@ -21,6 +21,8 @@ type IngressGatewayConfigEntry struct {
// what services to associated to those ports. // what services to associated to those ports.
Listeners []IngressListener Listeners []IngressListener
Meta map[string]string `json:",omitempty"`
// CreateIndex is the Raft index this entry was created at. This is a // CreateIndex is the Raft index this entry was created at. This is a
// read-only field. // read-only field.
CreateIndex uint64 CreateIndex uint64
@ -115,6 +117,8 @@ type TerminatingGatewayConfigEntry struct {
// Services is a list of service names represented by the terminating gateway. // Services is a list of service names represented by the terminating gateway.
Services []LinkedService `json:",omitempty"` Services []LinkedService `json:",omitempty"`
Meta map[string]string `json:",omitempty"`
// CreateIndex is the Raft index this entry was created at. This is a // CreateIndex is the Raft index this entry was created at. This is a
// read-only field. // read-only field.
CreateIndex uint64 CreateIndex uint64

View File

@ -271,6 +271,10 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "proxy-defaults", "Kind": "proxy-defaults",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Config": { "Config": {
"foo": 19, "foo": 19,
"bar": "abc", "bar": "abc",
@ -286,6 +290,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &ProxyConfigEntry{ expect: &ProxyConfigEntry{
Kind: "proxy-defaults", Kind: "proxy-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Config: map[string]interface{}{ Config: map[string]interface{}{
"foo": float64(19), "foo": float64(19),
"bar": "abc", "bar": "abc",
@ -304,6 +312,10 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "service-defaults", "Kind": "service-defaults",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Protocol": "http", "Protocol": "http",
"ExternalSNI": "abc-123", "ExternalSNI": "abc-123",
"MeshGateway": { "MeshGateway": {
@ -312,8 +324,12 @@ func TestDecodeConfigEntry(t *testing.T) {
} }
`, `,
expect: &ServiceConfigEntry{ expect: &ServiceConfigEntry{
Kind: "service-defaults", Kind: "service-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Protocol: "http", Protocol: "http",
ExternalSNI: "abc-123", ExternalSNI: "abc-123",
MeshGateway: MeshGatewayConfig{ MeshGateway: MeshGatewayConfig{
@ -327,6 +343,10 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "service-router", "Kind": "service-router",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Routes": [ "Routes": [
{ {
"Match": { "Match": {
@ -407,6 +427,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &ServiceRouterConfigEntry{ expect: &ServiceRouterConfigEntry{
Kind: "service-router", Kind: "service-router",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Routes: []ServiceRoute{ Routes: []ServiceRoute{
{ {
Match: &ServiceRouteMatch{ Match: &ServiceRouteMatch{
@ -490,6 +514,10 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "service-splitter", "Kind": "service-splitter",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Splits": [ "Splits": [
{ {
"Weight": 99.1, "Weight": 99.1,
@ -506,6 +534,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &ServiceSplitterConfigEntry{ expect: &ServiceSplitterConfigEntry{
Kind: ServiceSplitter, Kind: ServiceSplitter,
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Splits: []ServiceSplit{ Splits: []ServiceSplit{
{ {
Weight: 99.1, Weight: 99.1,
@ -525,6 +557,10 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "service-resolver", "Kind": "service-resolver",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"DefaultSubset": "v1", "DefaultSubset": "v1",
"ConnectTimeout": "15s", "ConnectTimeout": "15s",
"Subsets": { "Subsets": {
@ -549,8 +585,12 @@ func TestDecodeConfigEntry(t *testing.T) {
} }
}`, }`,
expect: &ServiceResolverConfigEntry{ expect: &ServiceResolverConfigEntry{
Kind: "service-resolver", Kind: "service-resolver",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
DefaultSubset: "v1", DefaultSubset: "v1",
ConnectTimeout: 15 * time.Second, ConnectTimeout: 15 * time.Second,
Subsets: map[string]ServiceResolverSubset{ Subsets: map[string]ServiceResolverSubset{
@ -725,6 +765,10 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "ingress-gateway", "Kind": "ingress-gateway",
"Name": "ingress-web", "Name": "ingress-web",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Tls": { "Tls": {
"Enabled": true "Enabled": true
}, },
@ -757,6 +801,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &IngressGatewayConfigEntry{ expect: &IngressGatewayConfigEntry{
Kind: "ingress-gateway", Kind: "ingress-gateway",
Name: "ingress-web", Name: "ingress-web",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
TLS: GatewayTLSConfig{ TLS: GatewayTLSConfig{
Enabled: true, Enabled: true,
}, },
@ -792,9 +840,13 @@ func TestDecodeConfigEntry(t *testing.T) {
{ {
"Kind": "terminating-gateway", "Kind": "terminating-gateway",
"Name": "terminating-west", "Name": "terminating-west",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Services": [ "Services": [
{ {
"Namespace": "foo", "Namespace": "foo",
"Name": "web", "Name": "web",
"CAFile": "/etc/ca.pem", "CAFile": "/etc/ca.pem",
"CertFile": "/etc/cert.pem", "CertFile": "/etc/cert.pem",
@ -813,6 +865,10 @@ func TestDecodeConfigEntry(t *testing.T) {
expect: &TerminatingGatewayConfigEntry{ expect: &TerminatingGatewayConfigEntry{
Kind: "terminating-gateway", Kind: "terminating-gateway",
Name: "terminating-west", Name: "terminating-west",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Services: []LinkedService{ Services: []LinkedService{
{ {
Namespace: "foo", Namespace: "foo",

View File

@ -79,6 +79,7 @@ type LockOptions struct {
MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime
LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime
LockTryOnce bool // Optional, defaults to false which means try forever LockTryOnce bool // Optional, defaults to false which means try forever
LockDelay time.Duration // Optional, defaults to 15s
Namespace string `json:",omitempty"` // Optional, defaults to API client config, namespace of ACL token, or "default" namespace Namespace string `json:",omitempty"` // Optional, defaults to API client config, namespace of ACL token, or "default" namespace
} }
@ -351,8 +352,9 @@ func (l *Lock) createSession() (string, error) {
se := l.opts.SessionOpts se := l.opts.SessionOpts
if se == nil { if se == nil {
se = &SessionEntry{ se = &SessionEntry{
Name: l.opts.SessionName, Name: l.opts.SessionName,
TTL: l.opts.SessionTTL, TTL: l.opts.SessionTTL,
LockDelay: l.opts.LockDelay,
} }
} }
w := WriteOptions{Namespace: l.opts.Namespace} w := WriteOptions{Namespace: l.opts.Namespace}

View File

@ -288,6 +288,9 @@ func (c *cmd) run(args []string) int {
case err := <-agent.RetryJoinCh(): case err := <-agent.RetryJoinCh():
c.logger.Error("Retry join failed", "error", err) c.logger.Error("Retry join failed", "error", err)
return 1 return 1
case <-agent.Failed():
// The deferred Shutdown method will log the appropriate error
return 1
case <-agent.ShutdownCh(): case <-agent.ShutdownCh():
// agent is already down! // agent is already down!
return 0 return 0

View File

@ -162,6 +162,10 @@ func TestParseConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "proxy-defaults" kind = "proxy-defaults"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
config { config {
"foo" = 19 "foo" = 19
"bar" = "abc" "bar" = "abc"
@ -176,6 +180,10 @@ func TestParseConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "proxy-defaults" Kind = "proxy-defaults"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Config { Config {
"foo" = 19 "foo" = 19
"bar" = "abc" "bar" = "abc"
@ -191,6 +199,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"kind": "proxy-defaults", "kind": "proxy-defaults",
"name": "main", "name": "main",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"config": { "config": {
"foo": 19, "foo": 19,
"bar": "abc", "bar": "abc",
@ -207,6 +219,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"Kind": "proxy-defaults", "Kind": "proxy-defaults",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Config": { "Config": {
"foo": 19, "foo": 19,
"bar": "abc", "bar": "abc",
@ -222,6 +238,10 @@ func TestParseConfigEntry(t *testing.T) {
expect: &api.ProxyConfigEntry{ expect: &api.ProxyConfigEntry{
Kind: "proxy-defaults", Kind: "proxy-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Config: map[string]interface{}{ Config: map[string]interface{}{
"foo": 19, "foo": 19,
"bar": "abc", "bar": "abc",
@ -236,6 +256,10 @@ func TestParseConfigEntry(t *testing.T) {
expectJSON: &api.ProxyConfigEntry{ expectJSON: &api.ProxyConfigEntry{
Kind: "proxy-defaults", Kind: "proxy-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Config: map[string]interface{}{ Config: map[string]interface{}{
"foo": float64(19), // json decoding gives float64 instead of int here "foo": float64(19), // json decoding gives float64 instead of int here
"bar": "abc", "bar": "abc",
@ -254,6 +278,10 @@ func TestParseConfigEntry(t *testing.T) {
kind = "terminating-gateway" kind = "terminating-gateway"
name = "terminating-gw-west" name = "terminating-gw-west"
namespace = "default" namespace = "default"
meta {
"foo" = "bar"
"gir" = "zim"
}
services = [ services = [
{ {
name = "billing" name = "billing"
@ -273,6 +301,10 @@ func TestParseConfigEntry(t *testing.T) {
Kind = "terminating-gateway" Kind = "terminating-gateway"
Name = "terminating-gw-west" Name = "terminating-gw-west"
Namespace = "default" Namespace = "default"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Services = [ Services = [
{ {
Name = "billing" Name = "billing"
@ -293,6 +325,10 @@ func TestParseConfigEntry(t *testing.T) {
"kind": "terminating-gateway", "kind": "terminating-gateway",
"name": "terminating-gw-west", "name": "terminating-gw-west",
"namespace": "default", "namespace": "default",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"services": [ "services": [
{ {
"name": "billing", "name": "billing",
@ -314,6 +350,10 @@ func TestParseConfigEntry(t *testing.T) {
"Kind": "terminating-gateway", "Kind": "terminating-gateway",
"Name": "terminating-gw-west", "Name": "terminating-gw-west",
"Namespace": "default", "Namespace": "default",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Services": [ "Services": [
{ {
"Name": "billing", "Name": "billing",
@ -334,6 +374,10 @@ func TestParseConfigEntry(t *testing.T) {
Kind: "terminating-gateway", Kind: "terminating-gateway",
Name: "terminating-gw-west", Name: "terminating-gw-west",
Namespace: "default", Namespace: "default",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Services: []api.LinkedService{ Services: []api.LinkedService{
{ {
Name: "billing", Name: "billing",
@ -353,6 +397,10 @@ func TestParseConfigEntry(t *testing.T) {
Kind: "terminating-gateway", Kind: "terminating-gateway",
Name: "terminating-gw-west", Name: "terminating-gw-west",
Namespace: "default", Namespace: "default",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Services: []api.LinkedService{ Services: []api.LinkedService{
{ {
Name: "billing", Name: "billing",
@ -374,6 +422,10 @@ func TestParseConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-defaults" kind = "service-defaults"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
protocol = "http" protocol = "http"
external_sni = "abc-123" external_sni = "abc-123"
mesh_gateway { mesh_gateway {
@ -383,6 +435,10 @@ func TestParseConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-defaults" Kind = "service-defaults"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Protocol = "http" Protocol = "http"
ExternalSNI = "abc-123" ExternalSNI = "abc-123"
MeshGateway { MeshGateway {
@ -393,6 +449,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"kind": "service-defaults", "kind": "service-defaults",
"name": "main", "name": "main",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"protocol": "http", "protocol": "http",
"external_sni": "abc-123", "external_sni": "abc-123",
"mesh_gateway": { "mesh_gateway": {
@ -404,6 +464,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"Kind": "service-defaults", "Kind": "service-defaults",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Protocol": "http", "Protocol": "http",
"ExternalSNI": "abc-123", "ExternalSNI": "abc-123",
"MeshGateway": { "MeshGateway": {
@ -412,8 +476,12 @@ func TestParseConfigEntry(t *testing.T) {
} }
`, `,
expect: &api.ServiceConfigEntry{ expect: &api.ServiceConfigEntry{
Kind: "service-defaults", Kind: "service-defaults",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Protocol: "http", Protocol: "http",
ExternalSNI: "abc-123", ExternalSNI: "abc-123",
MeshGateway: api.MeshGatewayConfig{ MeshGateway: api.MeshGatewayConfig{
@ -426,6 +494,10 @@ func TestParseConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-router" kind = "service-router"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
routes = [ routes = [
{ {
match { match {
@ -505,6 +577,10 @@ func TestParseConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-router" Kind = "service-router"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Routes = [ Routes = [
{ {
Match { Match {
@ -585,6 +661,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"kind": "service-router", "kind": "service-router",
"name": "main", "name": "main",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"routes": [ "routes": [
{ {
"match": { "match": {
@ -672,6 +752,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"Kind": "service-router", "Kind": "service-router",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Routes": [ "Routes": [
{ {
"Match": { "Match": {
@ -758,6 +842,10 @@ func TestParseConfigEntry(t *testing.T) {
expect: &api.ServiceRouterConfigEntry{ expect: &api.ServiceRouterConfigEntry{
Kind: "service-router", Kind: "service-router",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Routes: []api.ServiceRoute{ Routes: []api.ServiceRoute{
{ {
Match: &api.ServiceRouteMatch{ Match: &api.ServiceRouteMatch{
@ -840,6 +928,10 @@ func TestParseConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-splitter" kind = "service-splitter"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
splits = [ splits = [
{ {
weight = 97.1 weight = 97.1
@ -859,6 +951,10 @@ func TestParseConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-splitter" Kind = "service-splitter"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Splits = [ Splits = [
{ {
Weight = 97.1 Weight = 97.1
@ -879,6 +975,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"kind": "service-splitter", "kind": "service-splitter",
"name": "main", "name": "main",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"splits": [ "splits": [
{ {
"weight": 97.1, "weight": 97.1,
@ -900,6 +1000,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"Kind": "service-splitter", "Kind": "service-splitter",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Splits": [ "Splits": [
{ {
"Weight": 97.1, "Weight": 97.1,
@ -920,6 +1024,10 @@ func TestParseConfigEntry(t *testing.T) {
expect: &api.ServiceSplitterConfigEntry{ expect: &api.ServiceSplitterConfigEntry{
Kind: api.ServiceSplitter, Kind: api.ServiceSplitter,
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
Splits: []api.ServiceSplit{ Splits: []api.ServiceSplit{
{ {
Weight: 97.1, Weight: 97.1,
@ -942,6 +1050,10 @@ func TestParseConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "service-resolver" kind = "service-resolver"
name = "main" name = "main"
meta {
"foo" = "bar"
"gir" = "zim"
}
default_subset = "v1" default_subset = "v1"
connect_timeout = "15s" connect_timeout = "15s"
subsets = { subsets = {
@ -967,6 +1079,10 @@ func TestParseConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "service-resolver" Kind = "service-resolver"
Name = "main" Name = "main"
Meta {
"foo" = "bar"
"gir" = "zim"
}
DefaultSubset = "v1" DefaultSubset = "v1"
ConnectTimeout = "15s" ConnectTimeout = "15s"
Subsets = { Subsets = {
@ -993,6 +1109,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"kind": "service-resolver", "kind": "service-resolver",
"name": "main", "name": "main",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"default_subset": "v1", "default_subset": "v1",
"connect_timeout": "15s", "connect_timeout": "15s",
"subsets": { "subsets": {
@ -1026,6 +1146,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"Kind": "service-resolver", "Kind": "service-resolver",
"Name": "main", "Name": "main",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"DefaultSubset": "v1", "DefaultSubset": "v1",
"ConnectTimeout": "15s", "ConnectTimeout": "15s",
"Subsets": { "Subsets": {
@ -1056,8 +1180,12 @@ func TestParseConfigEntry(t *testing.T) {
} }
`, `,
expect: &api.ServiceResolverConfigEntry{ expect: &api.ServiceResolverConfigEntry{
Kind: "service-resolver", Kind: "service-resolver",
Name: "main", Name: "main",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
DefaultSubset: "v1", DefaultSubset: "v1",
ConnectTimeout: 15 * time.Second, ConnectTimeout: 15 * time.Second,
Subsets: map[string]api.ServiceResolverSubset{ Subsets: map[string]api.ServiceResolverSubset{
@ -1645,6 +1773,10 @@ func TestParseConfigEntry(t *testing.T) {
snake: ` snake: `
kind = "ingress-gateway" kind = "ingress-gateway"
name = "ingress-web" name = "ingress-web"
meta {
"foo" = "bar"
"gir" = "zim"
}
tls { tls {
enabled = true enabled = true
} }
@ -1668,6 +1800,10 @@ func TestParseConfigEntry(t *testing.T) {
camel: ` camel: `
Kind = "ingress-gateway" Kind = "ingress-gateway"
Name = "ingress-web" Name = "ingress-web"
Meta {
"foo" = "bar"
"gir" = "zim"
}
Tls { Tls {
Enabled = true Enabled = true
} }
@ -1692,6 +1828,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"kind": "ingress-gateway", "kind": "ingress-gateway",
"name": "ingress-web", "name": "ingress-web",
"meta" : {
"foo": "bar",
"gir": "zim"
},
"tls": { "tls": {
"enabled": true "enabled": true
}, },
@ -1717,6 +1857,10 @@ func TestParseConfigEntry(t *testing.T) {
{ {
"Kind": "ingress-gateway", "Kind": "ingress-gateway",
"Name": "ingress-web", "Name": "ingress-web",
"Meta" : {
"foo": "bar",
"gir": "zim"
},
"Tls": { "Tls": {
"Enabled": true "Enabled": true
}, },
@ -1741,6 +1885,10 @@ func TestParseConfigEntry(t *testing.T) {
expect: &api.IngressGatewayConfigEntry{ expect: &api.IngressGatewayConfigEntry{
Kind: "ingress-gateway", Kind: "ingress-gateway",
Name: "ingress-web", Name: "ingress-web",
Meta: map[string]string{
"foo": "bar",
"gir": "zim",
},
TLS: api.GatewayTLSConfig{ TLS: api.GatewayTLSConfig{
Enabled: true, Enabled: true,
}, },

View File

@ -4,7 +4,6 @@ import (
"errors" "errors"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"net" "net"
"os" "os"
"os/exec" "os/exec"
@ -20,6 +19,7 @@ import (
proxyCmd "github.com/hashicorp/consul/command/connect/proxy" proxyCmd "github.com/hashicorp/consul/command/connect/proxy"
"github.com/hashicorp/consul/command/flags" "github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/ipaddr" "github.com/hashicorp/consul/ipaddr"
"github.com/hashicorp/consul/tlsutil"
) )
func New(ui cli.Ui) *cmd { func New(ui cli.Ui) *cmd {
@ -443,13 +443,11 @@ func (c *cmd) templateArgs() (*BootstrapTplArgs, error) {
} }
var caPEM string var caPEM string
if httpCfg.TLSConfig.CAFile != "" { pems, err := tlsutil.LoadCAs(httpCfg.TLSConfig.CAFile, httpCfg.TLSConfig.CAPath)
content, err := ioutil.ReadFile(httpCfg.TLSConfig.CAFile) if err != nil {
if err != nil { return nil, err
return nil, fmt.Errorf("Failed to read CA file: %s", err)
}
caPEM = strings.Replace(string(content), "\n", "\\n", -1)
} }
caPEM = strings.Replace(strings.Join(pems, ""), "\n", "\\n", -1)
return &BootstrapTplArgs{ return &BootstrapTplArgs{
GRPC: grpcAddr, GRPC: grpcAddr,

View File

@ -370,6 +370,46 @@ func TestGenerateConfig(t *testing.T) {
LocalAgentClusterName: xds.LocalAgentClusterName, LocalAgentClusterName: xds.LocalAgentClusterName,
}, },
}, },
{
Name: "missing-ca-path",
Flags: []string{"-proxy-id", "test-proxy", "-ca-path", "some/path"},
WantArgs: BootstrapTplArgs{
EnvoyVersion: defaultEnvoyVersion,
ProxyCluster: "test-proxy",
ProxyID: "test-proxy",
// Should resolve IP, note this might not resolve the same way
// everywhere which might make this test brittle but not sure what else
// to do.
GRPC: GRPC{
AgentAddress: "127.0.0.1",
AgentPort: "8502",
},
},
WantErr: "lstat some/path: no such file or directory",
},
{
Name: "existing-ca-path",
Flags: []string{"-proxy-id", "test-proxy", "-ca-path", "../../../test/ca_path/"},
Env: []string{"CONSUL_HTTP_SSL=1"},
WantArgs: BootstrapTplArgs{
EnvoyVersion: defaultEnvoyVersion,
ProxyCluster: "test-proxy",
ProxyID: "test-proxy",
// Should resolve IP, note this might not resolve the same way
// everywhere which might make this test brittle but not sure what else
// to do.
GRPC: GRPC{
AgentAddress: "127.0.0.1",
AgentPort: "8502",
AgentTLS: true,
},
AgentCAPEM: `-----BEGIN CERTIFICATE-----\nMIIFADCCAuqgAwIBAgIBATALBgkqhkiG9w0BAQswEzERMA8GA1UEAxMIQ2VydEF1\ndGgwHhcNMTUwNTExMjI0NjQzWhcNMjUwNTExMjI0NjU0WjATMREwDwYDVQQDEwhD\nZXJ0QXV0aDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALcMByyynHsA\n+K4PJwo5+XHygaEZAhPGvHiKQK2Cbc9NDm0ZTzx0rA/dRTZlvouhDyzcJHm+6R1F\nj6zQv7iaSC3qQtJiPnPsfZ+/0XhFZ3fQWMnfDiGbZpF1kJF01ofB6vnsuocFC0zG\naGC+SZiLAzs+QMP3Bebw1elCBIeoN+8NWnRYmLsYIaYGJGBSbNo/lCpLTuinofUn\nL3ehWEGv1INwpHnSVeN0Ml2GFe23d7PUlj/wNIHgUdpUR+KEJxIP3klwtsI3QpSH\nc4VjWdf4aIcka6K3IFuw+K0PUh3xAAPnMpAQOtCZk0AhF5rlvUbevC6jADxpKxLp\nOONmvCTer4LtyNURAoBH52vbK0r/DNcTpPEFV0IP66nXUFgkk0mRKsu8HTb4IOkC\nX3K4mp18EiWUUtrHZAnNct0iIniDBqKK0yhSNhztG6VakVt/1WdQY9Ey3mNtxN1O\nthqWFKdpKUzPKYC3P6PfVpiE7+VbWTLLXba+8BPe8BxWPsVkjJqGSGnCte4COusz\nM8/7bbTgifwJfsepwFtZG53tvwjWlO46Exl30VoDNTaIGvs1fO0GqJlh2A7FN5F2\nS1rS5VYHtPK8QdmUSvyq+7JDBc1HNT5I2zsIQbNcLwDTZ5EsbU6QR7NHDJKxjv/w\nbs3eTXJSSNcFD74wRU10pXjgE5wOFu9TAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIA\nBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQHazgZ3Puiuc6K2LzgcX5b6fAC\nPzAfBgNVHSMEGDAWgBQHazgZ3Puiuc6K2LzgcX5b6fACPzALBgkqhkiG9w0BAQsD\nggIBAEmeNrSUhpHg1I8dtfqu9hCU/6IZThjtcFA+QcPkkMa+Z1k0SOtsgW8MdlcA\ngCf5g5yQZ0DdpWM9nDB6xDIhQdccm91idHgf8wmpEHUj0an4uyn2ESCt8eqrAWf7\nAClYORCASTYfguJCxcfvwtI1uqaOeCxSOdmFay79UVitVsWeonbCRGsVgBDifJxw\nG2oCQqoYAmXPM4J6syk5GHhB1O9MMq+g1+hOx9s+XHyTui9FL4V+IUO1ygVqEQB5\nPSiRBvcIsajSGVao+vK0gf2XfcXzqr3y3NhBky9rFMp1g+ykb2yWekV4WiROJlCj\nTsWwWZDRyjiGahDbho/XW8JciouHZhJdjhmO31rqW3HdFviCTdXMiGk3GQIzz/Jg\nP+enOaHXoY9lcxzDvY9z1BysWBgNvNrMnVge/fLP9o+a0a0PRIIVl8T0Ef3zeg1O\nCLCSy/1Vae5Tx63ZTFvGFdOSusYkG9rlAUHXZE364JRCKzM9Bz0bM+t+LaO0MaEb\nYoxcXEPU+gB2IvmARpInN3oHexR6ekuYHVTRGdWrdmuHFzc7eFwygRqTFdoCCU+G\nQZEkd+lOEyv0zvQqYg+Jp0AEGz2B2zB53uBVECtn0EqrSdPtRzUBSByXVs6QhSXn\neVmy+z3U3MecP63X6oSPXekqSyZFuegXpNNuHkjNoL4ep2ix\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEtzCCA5+gAwIBAgIJAIewRMI8OnvTMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHDAa\nBgNVBAoTE0hhc2hpQ29ycCBUZXN0IENlcnQxDDAKBgNVBAsTA0RldjEWMBQGA1UE\nAxMNdGVzdC5pbnRlcm5hbDEgMB4GCSqGSIb3DQEJARYRdGVzdEBpbnRlcm5hbC5j\nb20wHhcNMTQwNDA3MTkwMTA4WhcNMjQwNDA0MTkwMTA4WjCBmDELMAkGA1UEBhMC\nVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRwwGgYDVQQK\nExNIYXNoaUNvcnAgVGVzdCBDZXJ0MQwwCgYDVQQLEwNEZXYxFjAUBgNVBAMTDXRl\nc3QuaW50ZXJuYWwxIDAeBgkqhkiG9w0BCQEWEXRlc3RAaW50ZXJuYWwuY29tMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrs6JK4NpiOItxrpNR/1ppUU\nmH7p2BgLCBZ6eHdclle9J56i68adt8J85zaqphCfz6VDP58DsFx+N50PZyjQaDsU\nd0HejRqfHRMtg2O+UQkv4Z66+Vo+gc6uGuANi2xMtSYDVTAqqzF48OOPQDgYkzcG\nxcFZzTRFFZt2vPnyHj8cHcaFo/NMNVh7C3yTXevRGNm9u2mrbxCEeiHzFC2WUnvg\nU2jQuC7Fhnl33Zd3B6d3mQH6O23ncmwxTcPUJe6xZaIRrDuzwUcyhLj5Z3faag/f\npFIIcHSiHRfoqHLGsGg+3swId/zVJSSDHr7pJUu7Cre+vZa63FqDaooqvnisrQID\nAQABo4IBADCB/TAdBgNVHQ4EFgQUo/nrOfqvbee2VklVKIFlyQEbuJUwgc0GA1Ud\nIwSBxTCBwoAUo/nrOfqvbee2VklVKIFlyQEbuJWhgZ6kgZswgZgxCzAJBgNVBAYT\nAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEcMBoGA1UE\nChMTSGFzaGlDb3JwIFRlc3QgQ2VydDEMMAoGA1UECxMDRGV2MRYwFAYDVQQDEw10\nZXN0LmludGVybmFsMSAwHgYJKoZIhvcNAQkBFhF0ZXN0QGludGVybmFsLmNvbYIJ\nAIewRMI8OnvTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADa9fV9h\ngjapBlkNmu64WX0Ufub5dsJrdHS8672P30S7ILB7Mk0W8sL65IezRsZnG898yHf9\n2uzmz5OvNTM9K380g7xFlyobSVq+6yqmmSAlA/ptAcIIZT727P5jig/DB7fzJM3g\njctDlEGOmEe50GQXc25VKpcpjAsNQi5ER5gowQ0v3IXNZs+yU+LvxLHc0rUJ/XSp\nlFCAMOqd5uRoMOejnT51G6krvLNzPaQ3N9jQfNVY4Q0zfs0M+6dRWvqfqB9Vyq8/\nPOLMld+HyAZEBk9zK3ZVIXx6XS4dkDnSNR91njLq7eouf6M7+7s/oMQZZRtAfQ6r\nwlW975rYa1ZqEdA=\n-----END CERTIFICATE-----\n`,
AdminAccessLogPath: "/dev/null",
AdminBindAddress: "127.0.0.1",
AdminBindPort: "19000",
LocalAgentClusterName: xds.LocalAgentClusterName,
},
},
{ {
Name: "custom-bootstrap", Name: "custom-bootstrap",
Flags: []string{"-proxy-id", "test-proxy"}, Flags: []string{"-proxy-id", "test-proxy"},

View File

@ -0,0 +1,125 @@
{
"admin": {
"access_log_path": "/dev/null",
"address": {
"socket_address": {
"address": "127.0.0.1",
"port_value": 19000
}
}
},
"node": {
"cluster": "test-proxy",
"id": "test-proxy",
"metadata": {
"namespace": "default",
"envoy_version": "1.15.0"
}
},
"static_resources": {
"clusters": [
{
"name": "local_agent",
"connect_timeout": "1s",
"type": "STATIC",
"tls_context": {
"common_tls_context": {
"validation_context": {
"trusted_ca": {
"inline_string": "-----BEGIN CERTIFICATE-----\nMIIFADCCAuqgAwIBAgIBATALBgkqhkiG9w0BAQswEzERMA8GA1UEAxMIQ2VydEF1\ndGgwHhcNMTUwNTExMjI0NjQzWhcNMjUwNTExMjI0NjU0WjATMREwDwYDVQQDEwhD\nZXJ0QXV0aDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALcMByyynHsA\n+K4PJwo5+XHygaEZAhPGvHiKQK2Cbc9NDm0ZTzx0rA/dRTZlvouhDyzcJHm+6R1F\nj6zQv7iaSC3qQtJiPnPsfZ+/0XhFZ3fQWMnfDiGbZpF1kJF01ofB6vnsuocFC0zG\naGC+SZiLAzs+QMP3Bebw1elCBIeoN+8NWnRYmLsYIaYGJGBSbNo/lCpLTuinofUn\nL3ehWEGv1INwpHnSVeN0Ml2GFe23d7PUlj/wNIHgUdpUR+KEJxIP3klwtsI3QpSH\nc4VjWdf4aIcka6K3IFuw+K0PUh3xAAPnMpAQOtCZk0AhF5rlvUbevC6jADxpKxLp\nOONmvCTer4LtyNURAoBH52vbK0r/DNcTpPEFV0IP66nXUFgkk0mRKsu8HTb4IOkC\nX3K4mp18EiWUUtrHZAnNct0iIniDBqKK0yhSNhztG6VakVt/1WdQY9Ey3mNtxN1O\nthqWFKdpKUzPKYC3P6PfVpiE7+VbWTLLXba+8BPe8BxWPsVkjJqGSGnCte4COusz\nM8/7bbTgifwJfsepwFtZG53tvwjWlO46Exl30VoDNTaIGvs1fO0GqJlh2A7FN5F2\nS1rS5VYHtPK8QdmUSvyq+7JDBc1HNT5I2zsIQbNcLwDTZ5EsbU6QR7NHDJKxjv/w\nbs3eTXJSSNcFD74wRU10pXjgE5wOFu9TAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIA\nBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQHazgZ3Puiuc6K2LzgcX5b6fAC\nPzAfBgNVHSMEGDAWgBQHazgZ3Puiuc6K2LzgcX5b6fACPzALBgkqhkiG9w0BAQsD\nggIBAEmeNrSUhpHg1I8dtfqu9hCU/6IZThjtcFA+QcPkkMa+Z1k0SOtsgW8MdlcA\ngCf5g5yQZ0DdpWM9nDB6xDIhQdccm91idHgf8wmpEHUj0an4uyn2ESCt8eqrAWf7\nAClYORCASTYfguJCxcfvwtI1uqaOeCxSOdmFay79UVitVsWeonbCRGsVgBDifJxw\nG2oCQqoYAmXPM4J6syk5GHhB1O9MMq+g1+hOx9s+XHyTui9FL4V+IUO1ygVqEQB5\nPSiRBvcIsajSGVao+vK0gf2XfcXzqr3y3NhBky9rFMp1g+ykb2yWekV4WiROJlCj\nTsWwWZDRyjiGahDbho/XW8JciouHZhJdjhmO31rqW3HdFviCTdXMiGk3GQIzz/Jg\nP+enOaHXoY9lcxzDvY9z1BysWBgNvNrMnVge/fLP9o+a0a0PRIIVl8T0Ef3zeg1O\nCLCSy/1Vae5Tx63ZTFvGFdOSusYkG9rlAUHXZE364JRCKzM9Bz0bM+t+LaO0MaEb\nYoxcXEPU+gB2IvmARpInN3oHexR6ekuYHVTRGdWrdmuHFzc7eFwygRqTFdoCCU+G\nQZEkd+lOEyv0zvQqYg+Jp0AEGz2B2zB53uBVECtn0EqrSdPtRzUBSByXVs6QhSXn\neVmy+z3U3MecP63X6oSPXekqSyZFuegXpNNuHkjNoL4ep2ix\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIIEtzCCA5+gAwIBAgIJAIewRMI8OnvTMA0GCSqGSIb3DQEBBQUAMIGYMQswCQYD\nVQQGEwJVUzELMAkGA1UECBMCQ0ExFjAUBgNVBAcTDVNhbiBGcmFuY2lzY28xHDAa\nBgNVBAoTE0hhc2hpQ29ycCBUZXN0IENlcnQxDDAKBgNVBAsTA0RldjEWMBQGA1UE\nAxMNdGVzdC5pbnRlcm5hbDEgMB4GCSqGSIb3DQEJARYRdGVzdEBpbnRlcm5hbC5j\nb20wHhcNMTQwNDA3MTkwMTA4WhcNMjQwNDA0MTkwMTA4WjCBmDELMAkGA1UEBhMC\nVVMxCzAJBgNVBAgTAkNBMRYwFAYDVQQHEw1TYW4gRnJhbmNpc2NvMRwwGgYDVQQK\nExNIYXNoaUNvcnAgVGVzdCBDZXJ0MQwwCgYDVQQLEwNEZXYxFjAUBgNVBAMTDXRl\nc3QuaW50ZXJuYWwxIDAeBgkqhkiG9w0BCQEWEXRlc3RAaW50ZXJuYWwuY29tMIIB\nIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxrs6JK4NpiOItxrpNR/1ppUU\nmH7p2BgLCBZ6eHdclle9J56i68adt8J85zaqphCfz6VDP58DsFx+N50PZyjQaDsU\nd0HejRqfHRMtg2O+UQkv4Z66+Vo+gc6uGuANi2xMtSYDVTAqqzF48OOPQDgYkzcG\nxcFZzTRFFZt2vPnyHj8cHcaFo/NMNVh7C3yTXevRGNm9u2mrbxCEeiHzFC2WUnvg\nU2jQuC7Fhnl33Zd3B6d3mQH6O23ncmwxTcPUJe6xZaIRrDuzwUcyhLj5Z3faag/f\npFIIcHSiHRfoqHLGsGg+3swId/zVJSSDHr7pJUu7Cre+vZa63FqDaooqvnisrQID\nAQABo4IBADCB/TAdBgNVHQ4EFgQUo/nrOfqvbee2VklVKIFlyQEbuJUwgc0GA1Ud\nIwSBxTCBwoAUo/nrOfqvbee2VklVKIFlyQEbuJWhgZ6kgZswgZgxCzAJBgNVBAYT\nAlVTMQswCQYDVQQIEwJDQTEWMBQGA1UEBxMNU2FuIEZyYW5jaXNjbzEcMBoGA1UE\nChMTSGFzaGlDb3JwIFRlc3QgQ2VydDEMMAoGA1UECxMDRGV2MRYwFAYDVQQDEw10\nZXN0LmludGVybmFsMSAwHgYJKoZIhvcNAQkBFhF0ZXN0QGludGVybmFsLmNvbYIJ\nAIewRMI8OnvTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADa9fV9h\ngjapBlkNmu64WX0Ufub5dsJrdHS8672P30S7ILB7Mk0W8sL65IezRsZnG898yHf9\n2uzmz5OvNTM9K380g7xFlyobSVq+6yqmmSAlA/ptAcIIZT727P5jig/DB7fzJM3g\njctDlEGOmEe50GQXc25VKpcpjAsNQi5ER5gowQ0v3IXNZs+yU+LvxLHc0rUJ/XSp\nlFCAMOqd5uRoMOejnT51G6krvLNzPaQ3N9jQfNVY4Q0zfs0M+6dRWvqfqB9Vyq8/\nPOLMld+HyAZEBk9zK3ZVIXx6XS4dkDnSNR91njLq7eouf6M7+7s/oMQZZRtAfQ6r\nwlW975rYa1ZqEdA=\n-----END CERTIFICATE-----\n"
}
}
}
},
"http2_protocol_options": {},
"hosts": [
{
"socket_address": {
"address": "127.0.0.1",
"port_value": 8502
}
}
]
}
]
},
"stats_config": {
"stats_tags": [
{
"regex": "^cluster\\.((?:([^.]+)~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)",
"tag_name": "consul.custom_hash"
},
{
"regex": "^cluster\\.((?:[^.]+~)?(?:([^.]+)\\.)?[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)",
"tag_name": "consul.service_subset"
},
{
"regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?([^.]+)\\.[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)",
"tag_name": "consul.service"
},
{
"regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.([^.]+)\\.[^.]+\\.[^.]+\\.[^.]+\\.consul\\.)",
"tag_name": "consul.namespace"
},
{
"regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.([^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)",
"tag_name": "consul.datacenter"
},
{
"regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.([^.]+)\\.[^.]+\\.consul\\.)",
"tag_name": "consul.routing_type"
},
{
"regex": "^cluster\\.((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.([^.]+)\\.consul\\.)",
"tag_name": "consul.trust_domain"
},
{
"regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+)\\.[^.]+\\.[^.]+\\.consul\\.)",
"tag_name": "consul.target"
},
{
"regex": "^cluster\\.(((?:[^.]+~)?(?:[^.]+\\.)?[^.]+\\.[^.]+\\.[^.]+\\.[^.]+\\.[^.]+)\\.consul\\.)",
"tag_name": "consul.full_target"
},
{
"tag_name": "local_cluster",
"fixed_value": "test-proxy"
}
],
"use_all_default_tags": true
},
"dynamic_resources": {
"lds_config": {
"ads": {}
},
"cds_config": {
"ads": {}
},
"ads_config": {
"api_type": "GRPC",
"grpc_services": {
"initial_metadata": [
{
"key": "x-consul-token",
"value": ""
}
],
"envoy_grpc": {
"cluster_name": "local_agent"
}
}
}
},
"layered_runtime": {
"layers": [
{
"name": "static_layer",
"static_layer": {
"envoy.deprecated_features:envoy.api.v2.Cluster.tls_context": true,
"envoy.deprecated_features:envoy.config.trace.v2.ZipkinConfig.HTTP_JSON_V1": true,
"envoy.deprecated_features:envoy.config.filter.network.http_connection_manager.v2.HttpConnectionManager.Tracing.operation_name": true
}
}
]
}
}

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/google/go-querystring v1.0.0 // indirect github.com/google/go-querystring v1.0.0 // indirect
github.com/google/gofuzz v1.1.0 github.com/google/gofuzz v1.1.0
github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2 github.com/google/tcpproxy v0.0.0-20180808230851-dfa16c61dad2
github.com/hashicorp/consul/api v1.6.0 github.com/hashicorp/consul/api v1.7.0
github.com/hashicorp/consul/sdk v0.6.0 github.com/hashicorp/consul/sdk v0.6.0
github.com/hashicorp/errwrap v1.0.0 github.com/hashicorp/errwrap v1.0.0
github.com/hashicorp/go-bexpr v0.1.2 github.com/hashicorp/go-bexpr v0.1.2

View File

@ -11,12 +11,11 @@ import (
const ( const (
testFileName = "Consul.log" testFileName = "Consul.log"
testDuration = 2 * time.Second testDuration = 50 * time.Millisecond
testBytes = 10 testBytes = 10
) )
func TestLogFile_timeRotation(t *testing.T) { func TestLogFile_timeRotation(t *testing.T) {
t.Parallel()
tempDir := testutil.TempDir(t, "LogWriterTime") tempDir := testutil.TempDir(t, "LogWriterTime")
logFile := LogFile{ logFile := LogFile{
fileName: testFileName, fileName: testFileName,
@ -24,7 +23,7 @@ func TestLogFile_timeRotation(t *testing.T) {
duration: testDuration, duration: testDuration,
} }
logFile.Write([]byte("Hello World")) logFile.Write([]byte("Hello World"))
time.Sleep(2 * time.Second) time.Sleep(3 * testDuration)
logFile.Write([]byte("Second File")) logFile.Write([]byte("Second File"))
want := 2 want := 2
if got, _ := ioutil.ReadDir(tempDir); len(got) != want { if got, _ := ioutil.ReadDir(tempDir); len(got) != want {
@ -33,7 +32,6 @@ func TestLogFile_timeRotation(t *testing.T) {
} }
func TestLogFile_openNew(t *testing.T) { func TestLogFile_openNew(t *testing.T) {
t.Parallel()
tempDir := testutil.TempDir(t, "LogWriterOpen") tempDir := testutil.TempDir(t, "LogWriterOpen")
logFile := LogFile{fileName: testFileName, logPath: tempDir, duration: testDuration} logFile := LogFile{fileName: testFileName, logPath: tempDir, duration: testDuration}
if err := logFile.openNew(); err != nil { if err := logFile.openNew(); err != nil {
@ -46,7 +44,6 @@ func TestLogFile_openNew(t *testing.T) {
} }
func TestLogFile_byteRotation(t *testing.T) { func TestLogFile_byteRotation(t *testing.T) {
t.Parallel()
tempDir := testutil.TempDir(t, "LogWriterBytes") tempDir := testutil.TempDir(t, "LogWriterBytes")
logFile := LogFile{ logFile := LogFile{
fileName: testFileName, fileName: testFileName,
@ -64,7 +61,6 @@ func TestLogFile_byteRotation(t *testing.T) {
} }
func TestLogFile_deleteArchives(t *testing.T) { func TestLogFile_deleteArchives(t *testing.T) {
t.Parallel()
tempDir := testutil.TempDir(t, "LogWriteDeleteArchives") tempDir := testutil.TempDir(t, "LogWriteDeleteArchives")
logFile := LogFile{ logFile := LogFile{
fileName: testFileName, fileName: testFileName,
@ -100,7 +96,6 @@ func TestLogFile_deleteArchives(t *testing.T) {
} }
func TestLogFile_deleteArchivesDisabled(t *testing.T) { func TestLogFile_deleteArchivesDisabled(t *testing.T) {
t.Parallel()
tempDir := testutil.TempDir(t, t.Name()) tempDir := testutil.TempDir(t, t.Name())
logFile := LogFile{ logFile := LogFile{
fileName: testFileName, fileName: testFileName,
@ -121,7 +116,6 @@ func TestLogFile_deleteArchivesDisabled(t *testing.T) {
} }
func TestLogFile_rotationDisabled(t *testing.T) { func TestLogFile_rotationDisabled(t *testing.T) {
t.Parallel()
tempDir := testutil.TempDir(t, t.Name()) tempDir := testutil.TempDir(t, t.Name())
logFile := LogFile{ logFile := LogFile{
fileName: testFileName, fileName: testFileName,

Some files were not shown because too many files have changed in this diff Show More