mirror of
https://github.com/status-im/consul.git
synced 2025-01-09 13:26:07 +00:00
72f991d8d3
* agent: remove agent cache dependency from service mesh leaf certificate management This extracts the leaf cert management from within the agent cache. This code was produced by the following process: 1. All tests in agent/cache, agent/cache-types, agent/auto-config, agent/consul/servercert were run at each stage. - The tests in agent matching .*Leaf were run at each stage. - The tests in agent/leafcert were run at each stage after they existed. 2. The former leaf cert Fetch implementation was extracted into a new package behind a "fake RPC" endpoint to make it look almost like all other cache type internals. 3. The old cache type was shimmed to use the fake RPC endpoint and generally cleaned up. 4. I selectively duplicated all of Get/Notify/NotifyCallback/Prepopulate from the agent/cache.Cache implementation over into the new package. This was renamed as leafcert.Manager. - Code that was irrelevant to the leaf cert type was deleted (inlining blocking=true, refresh=false) 5. Everything that used the leaf cert cache type (including proxycfg stuff) was shifted to use the leafcert.Manager instead. 6. agent/cache-types tests were moved and gently replumbed to execute as-is against a leafcert.Manager. 7. Inspired by some of the locking changes from derek's branch I split the fat lock into N+1 locks. 8. The waiter chan struct{} was eventually replaced with a singleflight.Group around cache updates, which was likely the biggest net structural change. 9. The awkward two layers or logic produced as a byproduct of marrying the agent cache management code with the leaf cert type code was slowly coalesced and flattened to remove confusion. 10. The .*Leaf tests from the agent package were copied and made to work directly against a leafcert.Manager to increase direct coverage. I have done a best effort attempt to port the previous leaf-cert cache type's tests over in spirit, as well as to take the e2e-ish tests in the agent package with Leaf in the test name and copy those into the agent/leafcert package to get more direct coverage, rather than coverage tangled up in the agent logic. There is no net-new test coverage, just coverage that was pushed around from elsewhere.
435 lines
11 KiB
Go
435 lines
11 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package autoconf
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"net"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
"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/leafcert"
|
|
"github.com/hashicorp/consul/agent/metadata"
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/consul/agent/token"
|
|
"github.com/hashicorp/consul/proto/private/pbautoconf"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
)
|
|
|
|
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) AutoEncryptCert() *x509.Certificate {
|
|
ret := m.Called()
|
|
cert, _ := ret.Get(0).(*x509.Certificate)
|
|
return cert
|
|
}
|
|
|
|
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 mockLeafCerts struct {
|
|
mock.Mock
|
|
|
|
lock sync.Mutex
|
|
watchers map[string][]mockWatcher
|
|
}
|
|
|
|
var _ LeafCertManager = (*mockLeafCerts)(nil)
|
|
|
|
func newMockLeafCerts(t *testing.T) *mockLeafCerts {
|
|
m := mockLeafCerts{
|
|
watchers: make(map[string][]mockWatcher),
|
|
}
|
|
m.Test(t)
|
|
return &m
|
|
}
|
|
|
|
func (m *mockLeafCerts) Notify(ctx context.Context, req *leafcert.ConnectCALeafRequest, correlationID string, ch chan<- cache.UpdateEvent) error {
|
|
ret := m.Called(ctx, req, correlationID, ch)
|
|
|
|
err := ret.Error(0)
|
|
if err == nil {
|
|
m.lock.Lock()
|
|
key := req.Key()
|
|
m.watchers[key] = append(m.watchers[key], mockWatcher{ch: ch, done: ctx.Done()})
|
|
m.lock.Unlock()
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (m *mockLeafCerts) Prepopulate(
|
|
ctx context.Context,
|
|
key string,
|
|
index uint64,
|
|
value *structs.IssuedCert,
|
|
authorityKeyID string,
|
|
) error {
|
|
// 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 := value.PrivateKeyPEM
|
|
value.PrivateKeyPEM = "redacted"
|
|
|
|
ret := m.Called(ctx, key, index, value, authorityKeyID)
|
|
|
|
if restore != "" {
|
|
value.PrivateKeyPEM = restore
|
|
}
|
|
return ret.Error(0)
|
|
}
|
|
|
|
func (m *mockLeafCerts) 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 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, peerName 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, peerName, 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
|
|
|
|
loader *configLoader
|
|
directRPC *mockDirectRPC
|
|
serverProvider *mockServerProvider
|
|
cache *mockCache
|
|
leafCerts *mockLeafCerts
|
|
tokens *mockTokenStore
|
|
tlsCfg *mockTLSConfigurator
|
|
enterpriseConfig *mockedEnterpriseConfig
|
|
}
|
|
|
|
func newMockedConfig(t *testing.T) *mockedConfig {
|
|
loader := setupRuntimeConfig(t)
|
|
directRPC := newMockDirectRPC(t)
|
|
serverProvider := newMockServerProvider(t)
|
|
mcache := newMockCache(t)
|
|
mleafs := newMockLeafCerts(t)
|
|
tokens := newMockTokenStore(t)
|
|
tlsCfg := newMockTLSConfigurator(t)
|
|
|
|
entConfig := newMockedEnterpriseConfig(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)
|
|
mleafs.AssertExpectations(t)
|
|
mcache.AssertExpectations(t)
|
|
tokens.AssertExpectations(t)
|
|
tlsCfg.AssertExpectations(t)
|
|
}
|
|
})
|
|
|
|
return &mockedConfig{
|
|
Config: Config{
|
|
Loader: loader.Load,
|
|
DirectRPC: directRPC,
|
|
ServerProvider: serverProvider,
|
|
Cache: mcache,
|
|
LeafCertManager: mleafs,
|
|
Tokens: tokens,
|
|
TLSConfigurator: tlsCfg,
|
|
Logger: testutil.Logger(t),
|
|
EnterpriseConfig: entConfig.EnterpriseConfig,
|
|
},
|
|
loader: loader,
|
|
directRPC: directRPC,
|
|
serverProvider: serverProvider,
|
|
cache: mcache,
|
|
leafCerts: mleafs,
|
|
tokens: tokens,
|
|
tlsCfg: tlsCfg,
|
|
|
|
enterpriseConfig: entConfig,
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
for _, root := range indexedRoots.Roots {
|
|
if len(root.IntermediateCerts) == 0 {
|
|
root.IntermediateCerts = nil
|
|
}
|
|
}
|
|
|
|
// 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 := leafcert.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"
|
|
|
|
// we should prepopulate the cache with the agents cert
|
|
m.leafCerts.On("Prepopulate",
|
|
mock.Anything,
|
|
leafReq.Key(),
|
|
copy.RaftIndex.ModifyIndex,
|
|
©,
|
|
ca.SigningKeyID,
|
|
).Return(nil).Once()
|
|
|
|
// when prepopulating the cert in the cache we grab the token so
|
|
// we should expect 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
|
|
}
|