ContainerImage.Pinniped/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go
Ryan Richard 7f99d78462 Fix bug where LDAP or AD status conditions were not updated correctly
When the LDAP and AD IDP watcher controllers encountered an update error
while trying to update the status conditions of the IDP resources, then
they would drop the computed desired new value of the condition on the
ground. Next time the controller ran it would not try to update the
condition again because it wants to use the cached settings and had
already forgotten the desired new value of the condition computed during
the previous run of the controller. This would leave the outdated value
of the condition on the IDP resource.

This bug would manifest in CI as random failures in which the expected
condition message and the actual condition message would refer to
different versions numbers of the bind secret. The actual condition
message would refer to an older version of the bind secret because the
update failed and then the new desired message got dropped on the
ground.

This commit changes the in-memory caching strategy to also cache the
computed condition messages, allowing the conditions to be updated
on the IDP resource during future calls to Sync() in the case of a
failed update.
2022-01-07 17:19:13 -08:00

276 lines
9.1 KiB
Go

// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
// Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders.
package ldapupstreamwatcher
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
corev1informers "k8s.io/client-go/informers/core/v1"
"k8s.io/klog/v2/klogr"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
idpinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/idp/v1alpha1"
pinnipedcontroller "go.pinniped.dev/internal/controller"
"go.pinniped.dev/internal/controller/conditionsutil"
"go.pinniped.dev/internal/controller/supervisorconfig/upstreamwatchers"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/upstreamldap"
)
const (
ldapControllerName = "ldap-upstream-observer"
)
type ldapUpstreamGenericLDAPImpl struct {
ldapIdentityProvider v1alpha1.LDAPIdentityProvider
}
func (g *ldapUpstreamGenericLDAPImpl) Spec() upstreamwatchers.UpstreamGenericLDAPSpec {
return &ldapUpstreamGenericLDAPSpec{g.ldapIdentityProvider}
}
func (g *ldapUpstreamGenericLDAPImpl) Namespace() string {
return g.ldapIdentityProvider.Namespace
}
func (g *ldapUpstreamGenericLDAPImpl) Name() string {
return g.ldapIdentityProvider.Name
}
func (g *ldapUpstreamGenericLDAPImpl) Generation() int64 {
return g.ldapIdentityProvider.Generation
}
func (g *ldapUpstreamGenericLDAPImpl) Status() upstreamwatchers.UpstreamGenericLDAPStatus {
return &ldapUpstreamGenericLDAPStatus{g.ldapIdentityProvider}
}
type ldapUpstreamGenericLDAPSpec struct {
ldapIdentityProvider v1alpha1.LDAPIdentityProvider
}
func (s *ldapUpstreamGenericLDAPSpec) Host() string {
return s.ldapIdentityProvider.Spec.Host
}
func (s *ldapUpstreamGenericLDAPSpec) TLSSpec() *v1alpha1.TLSSpec {
return s.ldapIdentityProvider.Spec.TLS
}
func (s *ldapUpstreamGenericLDAPSpec) BindSecretName() string {
return s.ldapIdentityProvider.Spec.Bind.SecretName
}
func (s *ldapUpstreamGenericLDAPSpec) UserSearch() upstreamwatchers.UpstreamGenericLDAPUserSearch {
return &ldapUpstreamGenericLDAPUserSearch{s.ldapIdentityProvider.Spec.UserSearch}
}
func (s *ldapUpstreamGenericLDAPSpec) GroupSearch() upstreamwatchers.UpstreamGenericLDAPGroupSearch {
return &ldapUpstreamGenericLDAPGroupSearch{s.ldapIdentityProvider.Spec.GroupSearch}
}
func (s *ldapUpstreamGenericLDAPSpec) DetectAndSetSearchBase(_ context.Context, config *upstreamldap.ProviderConfig) *v1alpha1.Condition {
config.GroupSearch.Base = s.ldapIdentityProvider.Spec.GroupSearch.Base
config.UserSearch.Base = s.ldapIdentityProvider.Spec.UserSearch.Base
return nil
}
type ldapUpstreamGenericLDAPUserSearch struct {
userSearch v1alpha1.LDAPIdentityProviderUserSearch
}
func (u *ldapUpstreamGenericLDAPUserSearch) Base() string {
return u.userSearch.Base
}
func (u *ldapUpstreamGenericLDAPUserSearch) Filter() string {
return u.userSearch.Filter
}
func (u *ldapUpstreamGenericLDAPUserSearch) UsernameAttribute() string {
return u.userSearch.Attributes.Username
}
func (u *ldapUpstreamGenericLDAPUserSearch) UIDAttribute() string {
return u.userSearch.Attributes.UID
}
type ldapUpstreamGenericLDAPGroupSearch struct {
groupSearch v1alpha1.LDAPIdentityProviderGroupSearch
}
func (g *ldapUpstreamGenericLDAPGroupSearch) Base() string {
return g.groupSearch.Base
}
func (g *ldapUpstreamGenericLDAPGroupSearch) Filter() string {
return g.groupSearch.Filter
}
func (g *ldapUpstreamGenericLDAPGroupSearch) GroupNameAttribute() string {
return g.groupSearch.Attributes.GroupName
}
type ldapUpstreamGenericLDAPStatus struct {
ldapIdentityProvider v1alpha1.LDAPIdentityProvider
}
func (s *ldapUpstreamGenericLDAPStatus) Conditions() []v1alpha1.Condition {
return s.ldapIdentityProvider.Status.Conditions
}
// UpstreamLDAPIdentityProviderICache is a thread safe cache that holds a list of validated upstream LDAP IDP configurations.
type UpstreamLDAPIdentityProviderICache interface {
SetLDAPIdentityProviders([]provider.UpstreamLDAPIdentityProviderI)
}
type ldapWatcherController struct {
cache UpstreamLDAPIdentityProviderICache
validatedSecretVersionsCache upstreamwatchers.ValidatedSettingsCacheI
ldapDialer upstreamldap.LDAPDialer
client pinnipedclientset.Interface
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer
secretInformer corev1informers.SecretInformer
}
// New instantiates a new controllerlib.Controller which will populate the provided UpstreamLDAPIdentityProviderICache.
func New(
idpCache UpstreamLDAPIdentityProviderICache,
client pinnipedclientset.Interface,
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer,
secretInformer corev1informers.SecretInformer,
withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller {
return newInternal(
idpCache,
// start with an empty cache
upstreamwatchers.NewValidatedSettingsCache(),
// 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(
idpCache UpstreamLDAPIdentityProviderICache,
validatedSecretVersionsCache upstreamwatchers.ValidatedSettingsCacheI,
ldapDialer upstreamldap.LDAPDialer,
client pinnipedclientset.Interface,
ldapIdentityProviderInformer idpinformers.LDAPIdentityProviderInformer,
secretInformer corev1informers.SecretInformer,
withInformer pinnipedcontroller.WithInformerOptionFunc,
) controllerlib.Controller {
c := ldapWatcherController{
cache: idpCache,
validatedSecretVersionsCache: validatedSecretVersionsCache,
ldapDialer: ldapDialer,
client: client,
ldapIdentityProviderInformer: ldapIdentityProviderInformer,
secretInformer: secretInformer,
}
return controllerlib.New(
controllerlib.Config{Name: ldapControllerName, Syncer: &c},
withInformer(
ldapIdentityProviderInformer,
pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()),
controllerlib.InformerOption{},
),
withInformer(
secretInformer,
pinnipedcontroller.MatchAnySecretOfTypeFilter(upstreamwatchers.LDAPBindAccountSecretType, pinnipedcontroller.SingletonQueue()),
controllerlib.InformerOption{},
),
)
}
// Sync implements controllerlib.Syncer.
func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error {
actualUpstreams, err := c.ldapIdentityProviderInformer.Lister().List(labels.Everything())
if err != nil {
return fmt.Errorf("failed to list LDAPIdentityProviders: %w", err)
}
requeue := false
validatedUpstreams := make([]provider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams))
for _, upstream := range actualUpstreams {
valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream)
if valid != nil {
validatedUpstreams = append(validatedUpstreams, valid)
}
if requestedRequeue {
requeue = true
}
}
c.cache.SetLDAPIdentityProviders(validatedUpstreams)
if requeue {
return controllerlib.ErrSyntheticRequeue
}
return nil
}
func (c *ldapWatcherController) validateUpstream(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider) (p provider.UpstreamLDAPIdentityProviderI, requeue bool) {
spec := upstream.Spec
config := &upstreamldap.ProviderConfig{
Name: upstream.Name,
ResourceUID: upstream.UID,
Host: spec.Host,
UserSearch: upstreamldap.UserSearchConfig{
Base: spec.UserSearch.Base,
Filter: spec.UserSearch.Filter,
UsernameAttribute: spec.UserSearch.Attributes.Username,
UIDAttribute: spec.UserSearch.Attributes.UID,
},
GroupSearch: upstreamldap.GroupSearchConfig{
Base: spec.GroupSearch.Base,
Filter: spec.GroupSearch.Filter,
GroupNameAttribute: spec.GroupSearch.Attributes.GroupName,
},
Dialer: c.ldapDialer,
}
conditions := upstreamwatchers.ValidateGenericLDAP(ctx, &ldapUpstreamGenericLDAPImpl{*upstream}, c.secretInformer, c.validatedSecretVersionsCache, config)
c.updateStatus(ctx, upstream, conditions.Conditions())
return upstreamwatchers.EvaluateConditions(conditions, config)
}
func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.LDAPIdentityProvider, conditions []*v1alpha1.Condition) {
log := klogr.New().WithValues("namespace", upstream.Namespace, "name", upstream.Name)
updated := upstream.DeepCopy()
hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log)
updated.Status.Phase = v1alpha1.LDAPPhaseReady
if hadErrorCondition {
updated.Status.Phase = v1alpha1.LDAPPhaseError
}
if equality.Semantic.DeepEqual(upstream, updated) {
return // nothing to update
}
_, err := c.client.
IDPV1alpha1().
LDAPIdentityProviders(upstream.Namespace).
UpdateStatus(ctx, updated, metav1.UpdateOptions{})
if err != nil {
log.Error(err, "failed to update status")
}
}