
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.
276 lines
9.1 KiB
Go
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")
|
|
}
|
|
}
|