Cache ResourceVersion of the validated bind Secret in memory

...instead of caching it in the text of the Condition message
This commit is contained in:
Ryan Richard 2021-05-13 15:22:36 -07:00
parent 514ee5b883
commit f5bf8978a3
2 changed files with 110 additions and 47 deletions

View File

@ -9,7 +9,6 @@ import (
"crypto/x509" "crypto/x509"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"regexp"
"time" "time"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
@ -44,10 +43,6 @@ const (
loadedTLSConfigurationMessage = "loaded TLS configuration" loadedTLSConfigurationMessage = "loaded TLS configuration"
) )
var (
secretVersionParser = regexp.MustCompile(` \[validated with Secret ".+" at version "(.+)"]`)
)
// UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations. // UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations.
type UpstreamLDAPIdentityProviderICache interface { type UpstreamLDAPIdentityProviderICache interface {
SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI) SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI)
@ -55,12 +50,23 @@ type UpstreamLDAPIdentityProviderICache interface {
type ldapWatcherController struct { type ldapWatcherController struct {
cache UpstreamLDAPIdentityProviderICache cache UpstreamLDAPIdentityProviderICache
validatedSecretVersionsCache *secretVersionCache
ldapDialer upstreamldap.LDAPDialer ldapDialer upstreamldap.LDAPDialer
client pinnipedclientset.Interface client pinnipedclientset.Interface
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer
secretInformer corev1informers.SecretInformer secretInformer corev1informers.SecretInformer
} }
// An in-memory cache with an entry for each LDAPIdentityProvider, to keep track of which ResourceVersion
// of the bind Secret was used during the most recent successful validation.
type secretVersionCache struct {
ResourceVersionsByName map[string]string
}
func newSecretVersionCache() *secretVersionCache {
return &secretVersionCache{ResourceVersionsByName: map[string]string{}}
}
// New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache. // New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache.
func New( func New(
idpCache UpstreamLDAPIdentityProviderICache, idpCache UpstreamLDAPIdentityProviderICache,
@ -69,12 +75,23 @@ func New(
secretInformer corev1informers.SecretInformer, secretInformer corev1informers.SecretInformer,
withInformer pinnipedcontroller.WithInformerOptionFunc, withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller { ) controllerlib.Controller {
// nil means to use a real production dialer when creating objects to add to the dynamicUpstreamIDPProvider cache. return newInternal(
return newInternal(idpCache, nil, client, ldapIdentityProviderInformer, secretInformer, withInformer) idpCache,
// start with an empty secretVersionCache
newSecretVersionCache(),
// nil means to use a real production dialer when creating objects to add to the cache
nil,
client,
ldapIdentityProviderInformer,
secretInformer,
withInformer,
)
} }
// For test dependency injection purposes.
func newInternal( func newInternal(
idpCache UpstreamLDAPIdentityProviderICache, idpCache UpstreamLDAPIdentityProviderICache,
validatedSecretVersionsCache *secretVersionCache,
ldapDialer upstreamldap.LDAPDialer, ldapDialer upstreamldap.LDAPDialer,
client pinnipedclientset.Interface, client pinnipedclientset.Interface,
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer, ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer,
@ -83,6 +100,7 @@ func newInternal(
) controllerlib.Controller { ) controllerlib.Controller {
c := ldapWatcherController{ c := ldapWatcherController{
cache: idpCache, cache: idpCache,
validatedSecretVersionsCache: validatedSecretVersionsCache,
ldapDialer: ldapDialer, ldapDialer: ldapDialer,
client: client, client: client,
ldapIdentityProviderInformer: ldapIdentityProviderInformer, ldapIdentityProviderInformer: ldapIdentityProviderInformer,
@ -113,21 +131,24 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error {
requeue := false requeue := false
validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams)) validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams))
for _, upstream := range actualUpstreams { for _, upstream := range actualUpstreams {
valid := c.validateUpstream(ctx.Context, upstream) valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream)
if valid == nil { if valid != nil {
requeue = true
} else {
validatedUpstreams = append(validatedUpstreams, valid) validatedUpstreams = append(validatedUpstreams, valid)
} }
if requestedRequeue {
requeue = true
} }
}
c.cache.SetLDAPIdentityProviders(validatedUpstreams) c.cache.SetLDAPIdentityProviders(validatedUpstreams)
if requeue { if requeue {
return controllerlib.ErrSyntheticRequeue return controllerlib.ErrSyntheticRequeue
} }
return nil return nil
} }
func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) provider.UpstreamLDAPIdentityProviderI { func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) {
spec := upstream.Spec spec := upstream.Spec
config := &upstreamldap.ProviderConfig{ config := &upstreamldap.ProviderConfig{
@ -148,20 +169,33 @@ func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *
conditions = append(conditions, secretValidCondition, tlsValidCondition) conditions = append(conditions, secretValidCondition, tlsValidCondition)
// No point in trying to connect to the server if the config was already determined to be invalid. // No point in trying to connect to the server if the config was already determined to be invalid.
var finishedConfigCondition *v1alpha1.Condition
if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue { if secretValidCondition.Status == v1alpha1.ConditionTrue && tlsValidCondition.Status == v1alpha1.ConditionTrue {
finishedConfigCondition := c.validateFinishedConfig(ctx, upstream, config, currentSecretVersion) finishedConfigCondition = c.validateFinishedConfig(ctx, upstream, config, currentSecretVersion)
// nil when there is no need to update this condition.
if finishedConfigCondition != nil { if finishedConfigCondition != nil {
conditions = append(conditions, finishedConfigCondition) conditions = append(conditions, finishedConfigCondition)
} }
} }
hadErrorCondition := c.updateStatus(ctx, upstream, conditions) c.updateStatus(ctx, upstream, conditions)
if hadErrorCondition {
return nil switch {
case secretValidCondition.Status != v1alpha1.ConditionTrue || tlsValidCondition.Status != v1alpha1.ConditionTrue:
// Invalid provider, so do not load it into the cache.
p = nil
requeue = true
case finishedConfigCondition != nil && finishedConfigCondition.Status != v1alpha1.ConditionTrue:
// Error but load it into the cache anyway, treating this condition failure more like a warning.
p = upstreamldap.New(*config)
// Try again hoping that the condition will improve.
requeue = true
default:
// Fully validated provider, so load it into the cache.
p = upstreamldap.New(*config)
requeue = false
} }
return upstreamldap.New(*config) return p, requeue
} }
func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition { func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
@ -191,14 +225,23 @@ func (c *ldapWatcherController) validateTLSConfig(upstream *v1alpha1.LDAPIdentit
func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition { func (c *ldapWatcherController) validateFinishedConfig(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, config *upstreamldap.ProviderConfig, currentSecretVersion string) *v1alpha1.Condition {
ldapProvider := upstreamldap.New(*config) ldapProvider := upstreamldap.New(*config)
if hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion) { if c.hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream, currentSecretVersion) {
return nil return nil
} }
testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout) testConnectionTimeout, cancelFunc := context.WithTimeout(ctx, testLDAPConnectionTimeout)
defer cancelFunc() defer cancelFunc()
return c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion) condition := c.testConnection(testConnectionTimeout, upstream, config, ldapProvider, currentSecretVersion)
if condition.Status == v1alpha1.ConditionTrue {
// Remember (in-memory for this pod) that the controller has successfully validated the LDAP provider
// using this version of the Secret. This is for performance reasons, to avoid attempting to connect to
// the LDAP server more than is needed. If the pod restarts, it will attempt this validation again.
c.validatedSecretVersionsCache.ResourceVersionsByName[upstream.GetName()] = currentSecretVersion
}
return condition
} }
func (c *ldapWatcherController) testConnection( func (c *ldapWatcherController) testConnection(
@ -228,17 +271,13 @@ func (c *ldapWatcherController) testConnection(
} }
} }
func hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool { func (c *ldapWatcherController) hasPreviousSuccessfulConditionForCurrentSpecGenerationAndSecretVersion(upstream *v1alpha1.LDAPIdentityProvider, currentSecretVersion string) bool {
currentGeneration := upstream.Generation currentGeneration := upstream.Generation
for _, c := range upstream.Status.Conditions { for _, cond := range upstream.Status.Conditions {
if c.Type == typeLDAPConnectionValid && c.Status == v1alpha1.ConditionTrue && c.ObservedGeneration == currentGeneration { if cond.Type == typeLDAPConnectionValid && cond.Status == v1alpha1.ConditionTrue && cond.ObservedGeneration == currentGeneration {
// Found a previously successful condition for the current spec generation. // Found a previously successful condition for the current spec generation.
// Now figure out which version of the bind Secret was used during that previous validation. // Now figure out which version of the bind Secret was used during that previous validation, if any.
matches := secretVersionParser.FindStringSubmatch(c.Message) validatedSecretVersion := c.validatedSecretVersionsCache.ResourceVersionsByName[upstream.GetName()]
if len(matches) != 2 {
continue
}
validatedSecretVersion := matches[1]
if validatedSecretVersion == currentSecretVersion { if validatedSecretVersion == currentSecretVersion {
return true return true
} }
@ -308,7 +347,7 @@ func (c *ldapWatcherController) validateSecret(upstream *v1alpha1.LDAPIdentityPr
}, secret.ResourceVersion }, secret.ResourceVersion
} }
func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) bool { func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) {
log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name) log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name)
updated := upstream.DeepCopy() updated := upstream.DeepCopy()
@ -320,7 +359,7 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al
} }
if equality.Semantic.DeepEqual(upstream, updated) { if equality.Semantic.DeepEqual(upstream, updated) {
return hadErrorCondition return // nothing to update
} }
_, err := c.client. _, err := c.client.
@ -330,6 +369,4 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al
if err != nil { if err != nil {
log.Error(err, "failed to update status") log.Error(err, "failed to update status")
} }
return hadErrorCondition
} }

View File

@ -250,6 +250,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
initialValidatedSecretVersions map[string]string
inputUpstreams []runtime.Object inputUpstreams []runtime.Object
inputSecrets []runtime.Object inputSecrets []runtime.Object
setupMocks func(conn *mockldapconn.MockConn) setupMocks func(conn *mockldapconn.MockConn)
@ -257,6 +258,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
wantErr string wantErr string
wantResultingCache []*upstreamldap.ProviderConfig wantResultingCache []*upstreamldap.ProviderConfig
wantResultingUpstreams []v1alpha1.LDAPIdentityProvider wantResultingUpstreams []v1alpha1.LDAPIdentityProvider
wantValidatedSecretVersions map[string]string
}{ }{
{ {
name: "no LDAPIdentityProvider upstreams clears the cache", name: "no LDAPIdentityProvider upstreams clears the cache",
@ -279,6 +281,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "missing secret", name: "missing secret",
@ -455,6 +458,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "non-nil TLS configuration with empty CertificateAuthorityData is valid", name: "non-nil TLS configuration with empty CertificateAuthorityData is valid",
@ -489,6 +493,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream", name: "one valid upstream and one invalid upstream updates the cache to include only the valid upstream",
@ -531,9 +536,10 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
}, },
}, },
}, },
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "when testing the connection to the LDAP server fails then the upstream is not added to the cache", name: "when testing the connection to the LDAP server fails then the upstream is still added to the cache anyway (treated like a warning)",
inputUpstreams: []runtime.Object{validUpstream}, inputUpstreams: []runtime.Object{validUpstream},
inputSecrets: []runtime.Object{validBindUserSecret("")}, inputSecrets: []runtime.Object{validBindUserSecret("")},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
@ -542,7 +548,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantErr: controllerlib.ErrSyntheticRequeue.Error(), wantErr: controllerlib.ErrSyntheticRequeue.Error(),
wantResultingCache: []*upstreamldap.ProviderConfig{}, wantResultingCache: []*upstreamldap.ProviderConfig{providerConfigForValidUpstream},
wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{ wantResultingUpstreams: []v1alpha1.LDAPIdentityProvider{{
ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234}, ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234},
Status: v1alpha1.LDAPIdentityProviderStatus{ Status: v1alpha1.LDAPIdentityProviderStatus{
@ -573,6 +579,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSecretVersions: map[string]string{testName: "4242"},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called. // Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called.
}, },
@ -584,6 +591,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again", name: "when the LDAP server connection was validated for an older resource generation, then try to validate it again",
@ -594,6 +602,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSecretVersions: map[string]string{testName: "4242"},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
@ -607,6 +616,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again", name: "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again",
@ -624,6 +634,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, inputSecrets: []runtime.Object{validBindUserSecret("4242")},
initialValidatedSecretVersions: map[string]string{testName: "1"},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
@ -637,6 +648,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
{ {
name: "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again", name: "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again",
@ -647,6 +659,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
} }
})}, })},
inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version! inputSecrets: []runtime.Object{validBindUserSecret("4242")}, // newer secret version!
initialValidatedSecretVersions: map[string]string{testName: "4241"}, // old version was validated
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
// Should perform a test dial and bind. // Should perform a test dial and bind.
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
@ -660,6 +673,7 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
Conditions: allConditionsTrue(1234, "4242"), Conditions: allConditionsTrue(1234, "4242"),
}, },
}}, }},
wantValidatedSecretVersions: map[string]string{testName: "4242"},
}, },
} }
@ -692,8 +706,14 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
return conn, nil return conn, nil
})} })}
validatedSecretVersionCache := newSecretVersionCache()
if tt.initialValidatedSecretVersions != nil {
validatedSecretVersionCache.ResourceVersionsByName = tt.initialValidatedSecretVersions
}
controller := newInternal( controller := newInternal(
cache, cache,
validatedSecretVersionCache,
dialer, dialer,
fakePinnipedClient, fakePinnipedClient,
pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(), pinnipedInformers.IDP().V1alpha1().LDAPIdentityProviders(),
@ -737,6 +757,12 @@ func TestLDAPUpstreamWatcherControllerSync(t *testing.T) {
// Require each separately to get a nice diff when the test fails. // Require each separately to get a nice diff when the test fails.
require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i]) require.Equal(t, tt.wantResultingUpstreams[i], normalizedActualUpstreams[i])
} }
// Check that the controller remembered which version of the secret it most recently validated successfully with.
if tt.wantValidatedSecretVersions == nil {
tt.wantValidatedSecretVersions = map[string]string{}
}
require.Equal(t, tt.wantValidatedSecretVersions, validatedSecretVersionCache.ResourceVersionsByName)
}) })
} }
} }