// Copyright 2020 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package supervisorconfig import ( "context" "fmt" "net/url" "strings" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/clock" "k8s.io/client-go/util/retry" "k8s.io/klog/v2" configv1alpha1 "go.pinniped.dev/generated/1.19/apis/config/v1alpha1" pinnipedclientset "go.pinniped.dev/generated/1.19/client/clientset/versioned" configinformers "go.pinniped.dev/generated/1.19/client/informers/externalversions/config/v1alpha1" pinnipedcontroller "go.pinniped.dev/internal/controller" "go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/oidc/provider" ) // ProvidersSetter can be notified of all known valid providers with its SetIssuer function. // If there are no longer any valid issuers, then it can be called with no arguments. // Implementations of this type should be thread-safe to support calls from multiple goroutines. type ProvidersSetter interface { SetProviders(oidcProviders ...*provider.OIDCProvider) } type oidcProviderConfigWatcherController struct { providerSetter ProvidersSetter clock clock.Clock client pinnipedclientset.Interface opcInformer configinformers.OIDCProviderConfigInformer } // NewOIDCProviderConfigWatcherController creates a controllerlib.Controller that watches // OIDCProviderConfig objects and notifies a callback object of the collection of provider configs. func NewOIDCProviderConfigWatcherController( providerSetter ProvidersSetter, clock clock.Clock, client pinnipedclientset.Interface, opcInformer configinformers.OIDCProviderConfigInformer, withInformer pinnipedcontroller.WithInformerOptionFunc, ) controllerlib.Controller { return controllerlib.New( controllerlib.Config{ Name: "OIDCProviderConfigWatcherController", Syncer: &oidcProviderConfigWatcherController{ providerSetter: providerSetter, clock: clock, client: client, opcInformer: opcInformer, }, }, withInformer( opcInformer, pinnipedcontroller.NoOpFilter(), controllerlib.InformerOption{}, ), ) } // Sync implements controllerlib.Syncer. func (c *oidcProviderConfigWatcherController) Sync(ctx controllerlib.Context) error { all, err := c.opcInformer.Lister().List(labels.Everything()) if err != nil { return err } issuerCounts := make(map[string]int) for _, opc := range all { issuerCounts[opc.Spec.Issuer] = issuerCounts[opc.Spec.Issuer] + 1 } errs := newMultiError() oidcProviders := make([]*provider.OIDCProvider, 0) for _, opc := range all { if issuerCount := issuerCounts[opc.Spec.Issuer]; issuerCount > 1 { if err := c.updateStatus( ctx.Context, opc.Namespace, opc.Name, configv1alpha1.DuplicateOIDCProviderStatus, "Duplicate issuer", ); err != nil { errs.add(fmt.Errorf("could not update status: %w", err)) } continue } issuerURL, err := url.Parse(opc.Spec.Issuer) if err != nil { if err := c.updateStatus( ctx.Context, opc.Namespace, opc.Name, configv1alpha1.InvalidOIDCProviderStatus, "Invalid issuer URL: "+err.Error(), ); err != nil { errs.add(fmt.Errorf("could not update status: %w", err)) } continue } oidcProvider := &provider.OIDCProvider{Issuer: issuerURL} err = oidcProvider.Validate() if err != nil { if err := c.updateStatus( ctx.Context, opc.Namespace, opc.Name, configv1alpha1.InvalidOIDCProviderStatus, "Invalid issuer: "+err.Error(), ); err != nil { errs.add(fmt.Errorf("could not update status: %w", err)) } continue } oidcProviders = append(oidcProviders, oidcProvider) if err := c.updateStatus( ctx.Context, opc.Namespace, opc.Name, configv1alpha1.SuccessOIDCProviderStatus, "Provider successfully created", ); err != nil { // errs.add(fmt.Errorf("could not update status: %w", err)) return fmt.Errorf("could not update status: %w", err) } } c.providerSetter.SetProviders(oidcProviders...) return errs.errOrNil() } func (c *oidcProviderConfigWatcherController) updateStatus( ctx context.Context, namespace, name string, status configv1alpha1.OIDCProviderStatus, message string, ) error { return retry.RetryOnConflict(retry.DefaultRetry, func() error { opc, err := c.client.ConfigV1alpha1().OIDCProviderConfigs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return fmt.Errorf("get failed: %w", err) } if opc.Status.Status == status && opc.Status.Message == message { return nil } klog.InfoS( "attempting status update", "openidproviderconfig", klog.KRef(namespace, name), "status", status, "message", message, ) opc.Status.Status = status opc.Status.Message = message _, err = c.client.ConfigV1alpha1().OIDCProviderConfigs(namespace).Update(ctx, opc, metav1.UpdateOptions{}) return err }) } type multiError []error func newMultiError() multiError { return make([]error, 0) } func (m *multiError) add(err error) { *m = append(*m, err) } func (m multiError) len() int { return len(m) } func (m multiError) Error() string { sb := strings.Builder{} fmt.Fprintf(&sb, "%d errors:", m.len()) for _, err := range m { fmt.Fprintf(&sb, "\n- %s", err.Error()) } return sb.String() } func (m multiError) errOrNil() error { if m.len() > 0 { return m } return nil }