From ef36b454ba6709061901d886a48a9ca5cb90b72d Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Wed, 21 Feb 2024 11:12:20 -0500
Subject: [PATCH 01/17] Improve WebhookAuthenticator Status and Validations

- Validate TLS Configuration
- Validate Endpoint
- Validate TLS Negotiation
  - Report status handshake negotiation with webhook
- Unit tests
- Integration tests
---
 .../v1alpha1/types_webhookauthenticator.go    |  17 +
 .../webhookcachefiller/webhookcachefiller.go  | 290 +++++++++++++++--
 .../webhookcachefiller_test.go                | 299 +++++++++++++++---
 .../controllermanager/prepare_controllers.go  |   4 +-
 ...cierge_webhookauthenticator_status_test.go |   4 +
 5 files changed, 552 insertions(+), 62 deletions(-)
 create mode 100644 test/integration/concierge_webhookauthenticator_status_test.go

diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..a78e7a7e5 100644
--- a/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -5,6 +5,19 @@ package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index c7b24cc6f..191f7c77b 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -5,12 +5,17 @@
 package webhookcachefiller
 
 import (
+	"context"
+	"crypto/x509"
 	"fmt"
+	"net/url"
 	"os"
 
-	"github.com/go-logr/logr"
 	k8sauthv1beta1 "k8s.io/api/authentication/v1beta1"
+	"k8s.io/apimachinery/pkg/api/equality"
 	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	errorsutil "k8s.io/apimachinery/pkg/util/errors"
 	"k8s.io/apimachinery/pkg/util/net"
 	"k8s.io/apiserver/pkg/authentication/authenticator"
 	webhookutil "k8s.io/apiserver/pkg/util/webhook"
@@ -18,24 +23,56 @@ import (
 	"k8s.io/client-go/tools/clientcmd"
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 	"k8s.io/klog/v2"
+	"k8s.io/utils/clock"
 
 	auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
+	conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
 	authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
 	pinnipedcontroller "go.pinniped.dev/internal/controller"
 	pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
 	"go.pinniped.dev/internal/controller/authenticator/authncache"
+	"go.pinniped.dev/internal/controller/conditionsutil"
 	"go.pinniped.dev/internal/controllerlib"
+	"go.pinniped.dev/internal/plog"
+)
+
+const (
+	controllerName                   = "webhookcachefiller-controller"
+	typeReady                        = "Ready"
+	typeTLSConfigurationValid        = "TLSConfigurationValid"
+	typeEndpointURLValid             = "EndpointURLValid"
+	typeEndpointPOSTValid            = "EndpointPOSTValid"
+	typeAuthenticatorValid           = "AuthenticatorValid"
+	reasonSuccess                    = "Success"
+	reasonNotReady                   = "NotReady"
+	reasonUnableToValidate           = "UnableToValidate"
+	reasonUnableToCreateTempFile     = "UnableToCreateTempFile"
+	reasonUnableToMarshallKubeconfig = "UnableToMarshallKubeconfig"
+	reasonUnableToLoadKubeconfig     = "UnableToLoadKubeconfig"
+	reasonUnableToInstantiateWebhook = "UnableToInstantiateWebhook"
+	reasonInvalidTLSConfiguration    = "InvalidTLSConfiguration"
+	reasonInvalidEndpointURL         = "InvalidEndpointURL"
+	reasonInvalidEndpointURLScheme   = "InvalidEndpointURLScheme"
+	msgUnableToValidate              = "unable to validate; other issues present"
 )
 
 // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
-func New(cache *authncache.Cache, webhooks authinformers.WebhookAuthenticatorInformer, log logr.Logger) controllerlib.Controller {
+func New(
+	cache *authncache.Cache,
+	client conciergeclientset.Interface,
+	webhooks authinformers.WebhookAuthenticatorInformer,
+	clock clock.Clock,
+	log plog.Logger,
+) controllerlib.Controller {
 	return controllerlib.New(
 		controllerlib.Config{
-			Name: "webhookcachefiller-controller",
-			Syncer: &controller{
+			Name: controllerName,
+			Syncer: &webhookCacheFillerController{
 				cache:    cache,
+				client:   client,
 				webhooks: webhooks,
-				log:      log.WithName("webhookcachefiller-controller"),
+				clock:    clock,
+				log:      log.WithName(controllerName),
 			},
 		},
 		controllerlib.WithInformer(
@@ -46,14 +83,16 @@ func New(cache *authncache.Cache, webhooks authinformers.WebhookAuthenticatorInf
 	)
 }
 
-type controller struct {
+type webhookCacheFillerController struct {
 	cache    *authncache.Cache
 	webhooks authinformers.WebhookAuthenticatorInformer
-	log      logr.Logger
+	client   conciergeclientset.Interface
+	clock    clock.Clock
+	log      plog.Logger
 }
 
 // Sync implements controllerlib.Syncer.
-func (c *controller) Sync(ctx controllerlib.Context) error {
+func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 	obj, err := c.webhooks.Lister().Get(ctx.Key.Name)
 	if err != nil && errors.IsNotFound(err) {
 		c.log.Info("Sync() found that the WebhookAuthenticator does not exist yet or was deleted")
@@ -63,10 +102,22 @@ func (c *controller) Sync(ctx controllerlib.Context) error {
 		return fmt.Errorf("failed to get WebhookAuthenticator %s/%s: %w", ctx.Key.Namespace, ctx.Key.Name, err)
 	}
 
-	webhookAuthenticator, err := newWebhookAuthenticator(&obj.Spec, os.CreateTemp, clientcmd.WriteToFile)
-	if err != nil {
-		return fmt.Errorf("failed to build webhook config: %w", err)
-	}
+	conditions := make([]*metav1.Condition, 0)
+	specCopy := obj.Spec.DeepCopy()
+	var errs []error
+
+	_, conditions, tlsOk := c.validateTLS(specCopy.TLS, conditions)
+	_, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
+	conditions, endpointPOSTOk := c.validateEndpointPOST(specCopy.Endpoint, conditions, tlsOk && endpointOk)
+
+	webhookAuthenticator, conditions, err := newWebhookAuthenticator(
+		&obj.Spec,
+		os.CreateTemp,
+		clientcmd.WriteToFile,
+		conditions,
+		tlsOk && endpointOk && endpointPOSTOk,
+	)
+	errs = append(errs, err)
 
 	c.cache.Store(authncache.Key{
 		APIGroup: auth1alpha1.GroupName,
@@ -74,7 +125,15 @@ func (c *controller) Sync(ctx controllerlib.Context) error {
 		Name:     ctx.Key.Name,
 	}, webhookAuthenticator)
 	c.log.WithValues("webhook", klog.KObj(obj), "endpoint", obj.Spec.Endpoint).Info("added new webhook authenticator")
-	return nil
+	err = c.updateStatus(ctx.Context, obj, conditions)
+	errs = append(errs, err)
+
+	// sync loop errors:
+	// - should not be configuration errors. config errors a user must correct belong on the .Status
+	//   object. The controller simply must wait for a user to correct before running again.
+	// - other errors, such as networking errors, etc. are the types of errors that should return here
+	//   and signal the controller to retry the sync loop. These may be corrected by machines.
+	return errorsutil.NewAggregate(errs)
 }
 
 // newWebhookAuthenticator creates a webhook from the provided API server url and caBundle
@@ -83,17 +142,44 @@ func newWebhookAuthenticator(
 	spec *auth1alpha1.WebhookAuthenticatorSpec,
 	tempfileFunc func(string, string) (*os.File, error),
 	marshalFunc func(clientcmdapi.Config, string) error,
-) (*webhook.WebhookTokenAuthenticator, error) {
+	conditions []*metav1.Condition,
+	prereqOk bool,
+) (*webhook.WebhookTokenAuthenticator, []*metav1.Condition, error) {
+	if !prereqOk {
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeAuthenticatorValid,
+			Status:  metav1.ConditionUnknown,
+			Reason:  reasonUnableToValidate,
+			Message: msgUnableToValidate,
+		})
+		return nil, conditions, nil
+	}
 	temp, err := tempfileFunc("", "pinniped-webhook-kubeconfig-*")
 	if err != nil {
-		return nil, fmt.Errorf("unable to create temporary file: %w", err)
+		errText := "unable to create temporary file"
+		msg := fmt.Sprintf("%s: %s", errText, err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeAuthenticatorValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonUnableToCreateTempFile,
+			Message: msg,
+		})
+		return nil, conditions, fmt.Errorf("%s: %w", errText, err)
 	}
 	defer func() { _ = os.Remove(temp.Name()) }()
 
 	cluster := &clientcmdapi.Cluster{Server: spec.Endpoint}
 	_, cluster.CertificateAuthorityData, err = pinnipedauthenticator.CABundle(spec.TLS)
 	if err != nil {
-		return nil, fmt.Errorf("invalid TLS configuration: %w", err)
+		errText := "invalid TLS configuration"
+		msg := fmt.Sprintf("%s: %s", errText, err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeAuthenticatorValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidTLSConfiguration,
+			Message: msg,
+		})
+		return nil, conditions, fmt.Errorf("%s: %w", errText, err)
 	}
 
 	kubeconfig := clientcmdapi.NewConfig()
@@ -102,7 +188,15 @@ func newWebhookAuthenticator(
 	kubeconfig.CurrentContext = "anonymous"
 
 	if err := marshalFunc(*kubeconfig, temp.Name()); err != nil {
-		return nil, fmt.Errorf("unable to marshal kubeconfig: %w", err)
+		errText := "unable to marshal kubeconfig"
+		msg := fmt.Sprintf("%s: %s", errText, err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeAuthenticatorValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonUnableToMarshallKubeconfig,
+			Message: msg,
+		})
+		return nil, conditions, fmt.Errorf("%s: %w", errText, err)
 	}
 
 	// We use v1beta1 instead of v1 since v1beta1 is more prevalent in our desired
@@ -136,10 +230,166 @@ func newWebhookAuthenticator(
 	//  then use client.JSONConfig as clientConfig
 	clientConfig, err := webhookutil.LoadKubeconfig(temp.Name(), customDial)
 	if err != nil {
-		return nil, err
+		errText := "unable to load kubeconfig"
+		msg := fmt.Sprintf("%s: %s", errText, err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeAuthenticatorValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonUnableToLoadKubeconfig,
+			Message: msg,
+		})
+		return nil, conditions, fmt.Errorf("%s: %w", errText, err)
 	}
 
 	// this uses a http client that does not honor our TLS config
-	// TODO fix when we pick up https://github.com/kubernetes/kubernetes/pull/106155
-	return webhook.New(clientConfig, version, implicitAuds, *webhook.DefaultRetryBackoff())
+	// TODO: fix when we pick up https://github.com/kubernetes/kubernetes/pull/106155
+	//   NOTE: looks like the above was merged on Mar 18, 2022
+	webhookA, err := webhook.New(clientConfig, version, implicitAuds, *webhook.DefaultRetryBackoff())
+	if err != nil {
+		errText := "unable to instantiate webhook"
+		msg := fmt.Sprintf("%s: %s", errText, err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeAuthenticatorValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonUnableToInstantiateWebhook,
+			Message: msg,
+		})
+		return nil, conditions, fmt.Errorf("%s: %w", errText, err)
+	}
+	msg := "authenticator initialized"
+	conditions = append(conditions, &metav1.Condition{
+		Type:    typeAuthenticatorValid,
+		Status:  metav1.ConditionTrue,
+		Reason:  reasonSuccess,
+		Message: msg,
+	})
+	return webhookA, conditions, nil
+}
+
+func (c *webhookCacheFillerController) validateTLS(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
+	rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec)
+	if err != nil {
+		msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeTLSConfigurationValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidTLSConfiguration,
+			Message: msg,
+		})
+		return rootCAs, conditions, false
+	}
+	msg := "valid TLS configuration"
+	conditions = append(conditions, &metav1.Condition{
+		Type:    typeTLSConfigurationValid,
+		Status:  metav1.ConditionTrue,
+		Reason:  reasonSuccess,
+		Message: msg,
+	})
+	return rootCAs, conditions, true
+}
+
+func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditions []*metav1.Condition) (*url.URL, []*metav1.Condition, bool) {
+	endpointURL, err := url.Parse(endpoint)
+	if err != nil {
+		msg := fmt.Sprintf("%s: %s", "spec.endpoint URL is invalid", err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeEndpointURLValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidEndpointURL,
+			Message: msg,
+		})
+		return nil, conditions, false
+	}
+
+	if endpointURL.Scheme != "https" {
+		msg := fmt.Sprintf("spec.issuer %s has invalid scheme, require 'https'", endpoint)
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeEndpointURLValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidEndpointURLScheme,
+			Message: msg,
+		})
+		return nil, conditions, false
+	}
+
+	conditions = append(conditions, &metav1.Condition{
+		Type:    typeEndpointURLValid,
+		Status:  metav1.ConditionTrue,
+		Reason:  reasonSuccess,
+		Message: "endpoint is a valid URL",
+	})
+	return endpointURL, conditions, true
+}
+
+func (c *webhookCacheFillerController) validateEndpointPOST(endpoint string, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, bool) {
+	if endpoint == "" {
+		// TODO(BEN): do something with this. time to validate the endpoint will receive a POST
+		fmt.Println("FIX THIS")
+	}
+	if !prereqOk {
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeEndpointPOSTValid,
+			Status:  metav1.ConditionUnknown,
+			Reason:  reasonUnableToValidate,
+			Message: msgUnableToValidate,
+		})
+		return conditions, false
+	}
+
+	// TODO: do some things here so this func makes sense.
+	return conditions, false
+}
+
+func (c *webhookCacheFillerController) updateStatus(
+	ctx context.Context,
+	original *auth1alpha1.WebhookAuthenticator,
+	conditions []*metav1.Condition,
+) error {
+
+	updated := original.DeepCopy()
+
+	if hadErrorCondition(conditions) {
+		updated.Status.Phase = auth1alpha1.WebhookAuthenticatorPhaseError
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeReady,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonNotReady,
+			Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+		})
+	} else {
+		updated.Status.Phase = auth1alpha1.WebhookAuthenticatorPhaseReady
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeReady,
+			Status:  metav1.ConditionTrue,
+			Reason:  reasonSuccess,
+			Message: "the WebhookAuthenticator is ready",
+		})
+	}
+
+	_ = conditionsutil.MergeConfigConditions(
+		conditions,
+		original.Generation,
+		&updated.Status.Conditions,
+		plog.New().WithName(controllerName),
+		metav1.NewTime(c.clock.Now()),
+	)
+
+	if equality.Semantic.DeepEqual(original, updated) {
+		return nil
+	}
+
+	_, err := c.client.AuthenticationV1alpha1().WebhookAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
+	if err != nil {
+		c.log.Info(fmt.Sprintf("ERROR: %v", err))
+	}
+	return err
+}
+
+func hadErrorCondition(conditions []*metav1.Condition) bool {
+	for _, c := range conditions {
+		if c.Status != metav1.ConditionTrue {
+			return true
+		}
+	}
+	return false
 }
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index 291358003..be5e369dd 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -4,6 +4,7 @@
 package webhookcachefiller
 
 import (
+	"bytes"
 	"context"
 	"encoding/base64"
 	"fmt"
@@ -11,42 +12,154 @@ import (
 	"net/http"
 	"os"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/tools/clientcmd"
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
+	clocktesting "k8s.io/utils/clock/testing"
 
 	auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 	pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
 	pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
 	"go.pinniped.dev/internal/controller/authenticator/authncache"
 	"go.pinniped.dev/internal/controllerlib"
+	"go.pinniped.dev/internal/plog"
 	"go.pinniped.dev/internal/testutil"
+	"go.pinniped.dev/internal/testutil/conditionstestutil"
 	"go.pinniped.dev/internal/testutil/testlogger"
 )
 
 func TestController(t *testing.T) {
 	t.Parallel()
 
+	goodEndpoint := "https://example.com"
+
+	nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local)
+	frozenMetav1Now := metav1.NewTime(nowDoesntMatter)
+	frozenClock := clocktesting.NewFakeClock(nowDoesntMatter)
+
+	happyReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "Ready",
+			Status:             "True",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "Success",
+			Message:            "the WebhookAuthenticator is ready",
+		}
+	}
+	// sadReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	// 	return metav1.Condition{
+	// 		Type:               "Ready",
+	// 		Status:             "False",
+	// 		ObservedGeneration: observedGeneration,
+	// 		LastTransitionTime: time,
+	// 		Reason:             "NotReady",
+	// 		Message:            "the WebhookAuthenticator is not ready: see other conditions for details",
+	// 	}
+	// }
+	happyAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "AuthenticatorValid",
+			Status:             "True",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "Success",
+			Message:            "authenticator initialized",
+		}
+	}
+	// unknownAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	// 	return metav1.Condition{
+	// 		Type:               "AuthenticatorValid",
+	// 		Status:             "Unknown",
+	// 		ObservedGeneration: observedGeneration,
+	// 		LastTransitionTime: time,
+	// 		Reason:             "UnableToValidate",
+	// 		Message:            "unable to validate; other issues present",
+	// 	}
+	// }
+	// sadAuthenticatorValid := func() metav1.Condition {}
+
+	happyTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSConfigurationValid",
+			Status:             "True",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "Success",
+			Message:            "valid TLS configuration",
+		}
+	}
+	// sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	// 	return metav1.Condition{
+	// 		Type:               "TLSConfigurationValid",
+	// 		Status:             "False",
+	// 		ObservedGeneration: observedGeneration,
+	// 		LastTransitionTime: time,
+	// 		Reason:             "InvalidTLSConfiguration",
+	// 		Message:            "invalid TLS configuration: illegal base64 data at input byte 7",
+	// 	}
+	// }
+
+	happyEndpointURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "EndpointURLValid",
+			Status:             "True",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "Success",
+			Message:            "endpoint is a valid URL",
+		}
+	}
+	// happyEndpointURLValidInvalid := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+	// 	return metav1.Condition{
+	// 		Type:               "EndpointURLValid",
+	// 		Status:             "False",
+	// 		ObservedGeneration: observedGeneration,
+	// 		LastTransitionTime: time,
+	// 		Reason:             "InvalidIssuerURL",
+	// 		Message:            fmt.Sprintf(`spec.endpoint URL is invalid: parse "%s": invalid character " " in host name`, issuer),
+	// 	}
+	// }
+
+	allHappyConditionsSuccess := func(endpoint string, someTime metav1.Time, observedGeneration int64) []metav1.Condition {
+
+		return conditionstestutil.SortByType([]metav1.Condition{
+			happyEndpointURLValid(someTime, observedGeneration),
+			happyAuthenticatorValid(someTime, observedGeneration),
+			happyReadyCondition(someTime, observedGeneration),
+			happyTLSConfigurationValid(someTime, observedGeneration),
+		})
+	}
+
 	tests := []struct {
-		name             string
-		syncKey          controllerlib.Key
-		webhooks         []runtime.Object
-		wantErr          string
-		wantLogs         []string
-		wantCacheEntries int
+		name                 string
+		syncKey              controllerlib.Key
+		webhooks             []runtime.Object
+		wantErr              string
+		wantLogs             []string
+		wantStatusConditions []metav1.Condition
+		wantStatusPhase      auth1alpha1.WebhookAuthenticatorPhase
+		wantCacheEntries     int
 	}{
 		{
-			name:    "not found",
+			name:    "404: webhook authenticator not found will abort sync loop and not write status",
 			syncKey: controllerlib.Key{Name: "test-name"},
-			wantLogs: []string{
-				`webhookcachefiller-controller "level"=0 "msg"="Sync() found that the WebhookAuthenticator does not exist yet or was deleted"`,
-			},
+			// TODO(BEN): we lost this line when swapping loggers. Is that ok?
+			//   did the JWTAuthenticator also lose it?  Should we ensure something exists otherwise?
+			// wantLogs: []string{
+			// 	`webhookcachefiller-controller "level"=0 "msg"="Sync() found that the WebhookAuthenticator does not exist yet or was deleted"`,
+			// },
 		},
+		// Existing code that was never tested. We would likely have to create a server with bad clients to
+		// simulate this.
+		// { name: "non-404 `failed to get webhook authenticator` for other API server reasons" }
 		{
-			name:    "invalid webhook",
+			//  will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error
+			name:    "invalid webhook will fail the sync loop and........????",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -60,8 +173,14 @@ func TestController(t *testing.T) {
 			},
 			wantErr: `failed to build webhook config: parse "http://invalid url": invalid character " " in host name`,
 		},
+		// TODO (BEN): add valid without CA?
+		{
+			name: "valid webhook without CA...",
+		}, {
+			name: "",
+		},
 		{
-			name:    "valid webhook",
+			name:    "valid webhook will complete sync loop successfully with success conditions and ready phase",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -69,15 +188,18 @@ func TestController(t *testing.T) {
 						Name: "test-name",
 					},
 					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: "https://example.com",
+						Endpoint: goodEndpoint,
 						TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: ""},
 					},
 				},
 			},
-			wantLogs: []string{
-				`webhookcachefiller-controller "level"=0 "msg"="added new webhook authenticator" "endpoint"="https://example.com" "webhook"={"name":"test-name"}`,
-			},
-			wantCacheEntries: 1,
+			// TODO(BEN): we lost this changing loggers, make sure its captured in conditions
+			// wantLogs: []string{
+			// 	`webhookcachefiller-controller "level"=0 "msg"="added new webhook authenticator" "endpoint"="https://example.com" "webhook"={"name":"test-name"}`,
+			// },
+			wantStatusConditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+			wantStatusPhase:      "Ready",
+			wantCacheEntries:     1,
 		},
 	}
 	for _, tt := range tests {
@@ -85,12 +207,20 @@ func TestController(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			fakeClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
-			informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
+			pinnipedAPIClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
+			informers := pinnipedinformers.NewSharedInformerFactory(pinnipedAPIClient, 0)
 			cache := authncache.New()
 			testLog := testlogger.NewLegacy(t) //nolint:staticcheck  // old test with lots of log statements
 
-			controller := New(cache, informers.Authentication().V1alpha1().WebhookAuthenticators(), testLog.Logger)
+			var log bytes.Buffer
+			logger := plog.TestLogger(t, &log)
+
+			controller := New(
+				cache,
+				pinnipedAPIClient,
+				informers.Authentication().V1alpha1().WebhookAuthenticators(),
+				frozenClock,
+				logger)
 
 			ctx, cancel := context.WithCancel(context.Background())
 			defer cancel()
@@ -105,54 +235,133 @@ func TestController(t *testing.T) {
 			} else {
 				require.NoError(t, err)
 			}
-			require.Equal(t, tt.wantLogs, testLog.Lines())
-			require.Equal(t, tt.wantCacheEntries, len(cache.Keys()))
+			require.Equal(t, tt.wantLogs, testLog.Lines(), "log lines should be correct")
+
+			if tt.webhooks != nil {
+				var webhookAuthSubject *auth1alpha1.WebhookAuthenticator
+				getCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+				defer cancel()
+				webhookAuthSubject, getErr := pinnipedAPIClient.AuthenticationV1alpha1().WebhookAuthenticators().Get(getCtx, "test-name", metav1.GetOptions{})
+				require.NoError(t, getErr)
+				require.Equal(t, tt.wantStatusConditions, webhookAuthSubject.Status.Conditions, "status.conditions must be correct")
+				require.Equal(t, tt.wantStatusPhase, webhookAuthSubject.Status.Phase, "status.phase should be correct")
+			}
+
+			require.Equal(t, tt.wantCacheEntries, len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", tt.wantCacheEntries, len(cache.Keys()), cache.Keys()))
 		})
 	}
 }
 
 func TestNewWebhookAuthenticator(t *testing.T) {
-	t.Run("temp file failure", func(t *testing.T) {
+	goodEndpoint := "https://example.com"
+
+	t.Run("prerequisites not ready, cannot create webhook authenticator", func(t *testing.T) {
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{}, os.CreateTemp, clientcmd.WriteToFile, conditions, false)
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "Unknown",
+				Reason:  "UnableToValidate",
+				Message: "unable to validate; other issues present",
+			},
+		}, conditions)
+		require.Nil(t, res)
+		require.Nil(t, err)
+	})
+
+	t.Run("temp file failure, cannot create webhook authenticator", func(t *testing.T) {
 		brokenTempFile := func(_ string, _ string) (*os.File, error) { return nil, fmt.Errorf("some temp file error") }
-		res, err := newWebhookAuthenticator(nil, brokenTempFile, clientcmd.WriteToFile)
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(nil, brokenTempFile, clientcmd.WriteToFile, conditions, true)
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "False",
+				Reason:  "UnableToCreateTempFile",
+				Message: "unable to create temporary file: some temp file error",
+			},
+		}, conditions)
 		require.Nil(t, res)
 		require.EqualError(t, err, "unable to create temporary file: some temp file error")
 	})
 
-	t.Run("marshal failure", func(t *testing.T) {
+	t.Run("marshal failure, cannot create webhook authenticator", func(t *testing.T) {
 		marshalError := func(_ clientcmdapi.Config, _ string) error { return fmt.Errorf("some marshal error") }
-		res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{}, os.CreateTemp, marshalError)
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{}, os.CreateTemp, marshalError, conditions, true)
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "False",
+				Reason:  "UnableToMarshallKubeconfig",
+				Message: "unable to marshal kubeconfig: some marshal error",
+			},
+		}, conditions)
 		require.Nil(t, res)
 		require.EqualError(t, err, "unable to marshal kubeconfig: some marshal error")
 	})
 
-	t.Run("invalid base64", func(t *testing.T) {
-		res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: "https://example.com",
+	// t.Run("load kubeconfig err, not currently tested, may not be reasonable to test?")
+
+	t.Run("invalid TLS config, base64 encoding err, cannot create webhook authenticator", func(t *testing.T) {
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
+			Endpoint: goodEndpoint,
 			TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid-base64"},
-		}, os.CreateTemp, clientcmd.WriteToFile)
+		}, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "False",
+				Reason:  "InvalidTLSConfiguration",
+				Message: "invalid TLS configuration: illegal base64 data at input byte 7",
+			},
+		}, conditions)
 		require.Nil(t, res)
+		// TODO: should this trigger the sync loop again with an error, or should this have been only
+		// status and log, indicating user must correct?
 		require.EqualError(t, err, "invalid TLS configuration: illegal base64 data at input byte 7")
 	})
 
-	t.Run("invalid pem data", func(t *testing.T) {
-		res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: "https://example.com",
+	t.Run("invalid pem data, cannot create webhook authenticator", func(t *testing.T) {
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
+			Endpoint: goodEndpoint,
 			TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("bad data"))},
-		}, os.CreateTemp, clientcmd.WriteToFile)
+		}, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "False",
+				Reason:  "InvalidTLSConfiguration",
+				Message: "invalid TLS configuration: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates",
+			},
+		}, conditions)
 		require.Nil(t, res)
 		require.EqualError(t, err, "invalid TLS configuration: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates")
 	})
 
-	t.Run("valid config with no TLS spec", func(t *testing.T) {
-		res, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: "https://example.com",
-		}, os.CreateTemp, clientcmd.WriteToFile)
+	t.Run("valid config with no TLS spec, webhook authenticator created", func(t *testing.T) {
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
+			Endpoint: goodEndpoint,
+		}, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "True",
+				Reason:  "Success",
+				Message: "authenticator initialized",
+			},
+		}, conditions)
 		require.NotNil(t, res)
 		require.NoError(t, err)
 	})
 
-	t.Run("success", func(t *testing.T) {
+	t.Run("success, webhook authenticator created", func(t *testing.T) {
+		// TODO(BEN): when enhancing webhook authenticator integration test, can prob
+		// steal this and create a super simpler server
 		caBundle, url := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
 			body, err := io.ReadAll(r.Body)
 			require.NoError(t, err)
@@ -166,10 +375,18 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 				CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(caBundle)),
 			},
 		}
-		res, err := newWebhookAuthenticator(spec, os.CreateTemp, clientcmd.WriteToFile)
+		conditions := []*metav1.Condition{}
+		res, conditions, err := newWebhookAuthenticator(spec, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
 		require.NoError(t, err)
 		require.NotNil(t, res)
-
+		require.Equal(t, []*metav1.Condition{
+			{
+				Type:    "AuthenticatorValid",
+				Status:  "True",
+				Reason:  "Success",
+				Message: "authenticator initialized",
+			},
+		}, conditions)
 		resp, authenticated, err := res.AuthenticateToken(context.Background(), "test-token")
 		require.NoError(t, err)
 		require.Nil(t, resp)
diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go
index 1a8e195d6..379db68a4 100644
--- a/internal/controllermanager/prepare_controllers.go
+++ b/internal/controllermanager/prepare_controllers.go
@@ -236,8 +236,10 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol
 		WithController(
 			webhookcachefiller.New(
 				c.AuthenticatorCache,
+				client.PinnipedConcierge,
 				informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(),
-				plog.Logr(), //nolint:staticcheck // old controller with lots of log statements
+				clock.RealClock{},
+				plog.New(),
 			),
 			singletonWorker,
 		).
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
new file mode 100644
index 000000000..099848b22
--- /dev/null
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -0,0 +1,4 @@
+// Copyright 2024 the Pinniped contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package integration

From 590e2d18f7d24a24b7226936ad7622613a13c2ee Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Wed, 21 Feb 2024 15:05:01 -0500
Subject: [PATCH 02/17] Add WebhookAuthenticator integration tests, expand unit
 tests

- Add WebhookAuthenticator unit tests, update generated code
- Add validateTLSNegotiation(), update tests
- Update validateTLSNegotiation, add unit tests, factor out helpers
- Update generated code
---
 .../types_webhookauthenticator.go.tmpl        |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.21/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.22/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.23/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.24/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.25/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.26/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.27/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.28/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/1.29/README.adoc                    |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |  19 +-
 ...ge.pinniped.dev_webhookauthenticators.yaml |   8 +
 generated/latest/README.adoc                  |  13 +
 .../v1alpha1/types_webhookauthenticator.go    |   2 +-
 .../jwtcachefiller/jwtcachefiller_test.go     |  46 +-
 .../webhookcachefiller/webhookcachefiller.go  | 147 ++-
 .../webhookcachefiller_test.go                | 913 ++++++++++++++++--
 .../controllermanager/prepare_controllers.go  |   2 +
 internal/testutil/testlogger/loglines.go      |  14 +
 ...cierge_webhookauthenticator_status_test.go |   6 +
 37 files changed, 1337 insertions(+), 193 deletions(-)
 create mode 100644 internal/testutil/testlogger/loglines.go

diff --git a/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl b/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl
index 207249b28..cbe3eeeb0 100644
--- a/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl
+++ b/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go.tmpl
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/deploy/concierge/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc
index 34dca9ce2..677bd9552 100644
--- a/generated/1.21/README.adoc
+++ b/generated/1.21/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.21/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.21/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.21/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.21/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.21/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.21/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 72b55d000..e762c8b5c 100644
--- a/generated/1.21/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.21/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -150,6 +150,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc
index f8943df40..de663d43f 100644
--- a/generated/1.22/README.adoc
+++ b/generated/1.22/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.22/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.22/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.22/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.22/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.22/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.22/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 72b55d000..e762c8b5c 100644
--- a/generated/1.22/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.22/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -150,6 +150,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc
index 8b0a61615..d542fc70c 100644
--- a/generated/1.23/README.adoc
+++ b/generated/1.23/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.23/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.23/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.23/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.23/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.23/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.23/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.23/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.23/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc
index 89c550839..782f1cc2b 100644
--- a/generated/1.24/README.adoc
+++ b/generated/1.24/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc
index 0f44a7538..cb137d202 100644
--- a/generated/1.25/README.adoc
+++ b/generated/1.25/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.25/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.25/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc
index 406eac404..17170f767 100644
--- a/generated/1.26/README.adoc
+++ b/generated/1.26/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.26/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.26/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc
index 9bafbbbff..026512700 100644
--- a/generated/1.27/README.adoc
+++ b/generated/1.27/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.27/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.27/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc
index 0aedac991..9f6f45886 100644
--- a/generated/1.28/README.adoc
+++ b/generated/1.28/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.28/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.28/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/1.29/README.adoc b/generated/1.29/README.adoc
index 71e2ffc09..dbecc10cf 100644
--- a/generated/1.29/README.adoc
+++ b/generated/1.29/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index 207249b28..cbe3eeeb0 100644
--- a/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/1.29/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,10 +1,23 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
 
 import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
+type WebhookAuthenticatorPhase string
+
+const (
+	// WebhookAuthenticatorPhasePending is the default phase for newly-created WebhookAuthenticator resources.
+	WebhookAuthenticatorPhasePending WebhookAuthenticatorPhase = "Pending"
+
+	// WebhookAuthenticatorPhaseReady is the phase for an WebhookAuthenticator resource in a healthy state.
+	WebhookAuthenticatorPhaseReady WebhookAuthenticatorPhase = "Ready"
+
+	// WebhookAuthenticatorPhaseError is the phase for an WebhookAuthenticator in an unhealthy state.
+	WebhookAuthenticatorPhaseError WebhookAuthenticatorPhase = "Error"
+)
+
 // Status of a webhook authenticator.
 type WebhookAuthenticatorStatus struct {
 	// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type WebhookAuthenticatorStatus struct {
 	// +listType=map
 	// +listMapKey=type
 	Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
+	// Phase summarizes the overall status of the WebhookAuthenticator.
+	// +kubebuilder:default=Pending
+	// +kubebuilder:validation:Enum=Pending;Ready;Error
+	Phase WebhookAuthenticatorPhase `json:"phase,omitempty"`
 }
 
 // Spec for configuring a webhook authenticator.
diff --git a/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml b/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
index 08defa182..e8cbc6790 100644
--- a/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
+++ b/generated/1.29/crds/authentication.concierge.pinniped.dev_webhookauthenticators.yaml
@@ -144,6 +144,14 @@ spec:
                 x-kubernetes-list-map-keys:
                 - type
                 x-kubernetes-list-type: map
+              phase:
+                default: Pending
+                description: Phase summarizes the overall status of the WebhookAuthenticator.
+                enum:
+                - Pending
+                - Ready
+                - Error
+                type: string
             type: object
         required:
         - spec
diff --git a/generated/latest/README.adoc b/generated/latest/README.adoc
index 71e2ffc09..dbecc10cf 100644
--- a/generated/latest/README.adoc
+++ b/generated/latest/README.adoc
@@ -154,6 +154,18 @@ WebhookAuthenticator describes the configuration of a webhook authenticator.
 
 
 
+[id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase"]
+==== WebhookAuthenticatorPhase (string) 
+
+
+
+.Appears In:
+****
+- xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorstatus[$$WebhookAuthenticatorStatus$$]
+****
+
+
+
 [id="{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorspec"]
 ==== WebhookAuthenticatorSpec 
 
@@ -186,6 +198,7 @@ Status of a webhook authenticator.
 |===
 | Field | Description
 | *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
+| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-29-apis-concierge-authentication-v1alpha1-webhookauthenticatorphase[$$WebhookAuthenticatorPhase$$]__ | Phase summarizes the overall status of the WebhookAuthenticator.
 |===
 
 
diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go b/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
index a78e7a7e5..cbe3eeeb0 100644
--- a/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
+++ b/generated/latest/apis/concierge/authentication/v1alpha1/types_webhookauthenticator.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package v1alpha1
diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index d87e71abf..0efc42354 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -10,10 +10,7 @@ import (
 	"crypto/elliptic"
 	"crypto/rand"
 	"crypto/rsa"
-	"crypto/tls"
-	"encoding/base64"
 	"encoding/json"
-	"encoding/pem"
 	"errors"
 	"fmt"
 	"net/http"
@@ -46,7 +43,9 @@ import (
 	"go.pinniped.dev/internal/mocks/mocktokenauthenticatorcloser"
 	"go.pinniped.dev/internal/plog"
 	"go.pinniped.dev/internal/testutil"
+	"go.pinniped.dev/internal/testutil/conciergetestutil"
 	"go.pinniped.dev/internal/testutil/conditionstestutil"
+	"go.pinniped.dev/internal/testutil/testlogger"
 	"go.pinniped.dev/internal/testutil/tlsserver"
 )
 
@@ -209,12 +208,12 @@ func TestController(t *testing.T) {
 	someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   goodIssuer,
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 	}
 	someJWTAuthenticatorSpecWithUsernameClaim := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   goodIssuer,
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 		Claims: auth1alpha1.JWTTokenClaims{
 			Username: "my-custom-username-claim",
 		},
@@ -222,7 +221,7 @@ func TestController(t *testing.T) {
 	someJWTAuthenticatorSpecWithGroupsClaim := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   goodIssuer,
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 		Claims: auth1alpha1.JWTTokenClaims{
 			Groups: customGroupsClaim,
 		},
@@ -248,12 +247,12 @@ func TestController(t *testing.T) {
 	invalidIssuerJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   "https://.café   .com/café/café/café/coffee",
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 	}
 	invalidIssuerSchemeJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   "http://.café.com/café/café/café/coffee",
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 	}
 
 	validIssuerURLButDoesNotExistJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
@@ -263,18 +262,18 @@ func TestController(t *testing.T) {
 	badIssuerJWKSURIJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   badIssuerInvalidJWKSURI,
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURI.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURI.TLS),
 	}
 	badIssuerJWKSURISchemeJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   badIssuerInvalidJWKSURIScheme,
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIScheme.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIScheme.TLS),
 	}
 
 	jwksFetchShouldFailJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   jwksFetchShouldFailServer.URL,
 		Audience: goodAudience,
-		TLS:      tlsSpecFromTLSConfig(jwksFetchShouldFailServer.TLS),
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(jwksFetchShouldFailServer.TLS),
 	}
 
 	happyReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
@@ -1455,7 +1454,7 @@ func TestController(t *testing.T) {
 				require.NoError(t, err)
 			}
 
-			actualLogLines := logLines(log.String())
+			actualLogLines := testlogger.LogLines(log.String())
 			require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct")
 
 			for logLineNum, logLine := range actualLogLines {
@@ -1800,21 +1799,6 @@ func testTableForAuthenticateTokenTests(
 	return tests
 }
 
-func tlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
-	pemData := make([]byte, 0)
-	for _, certificate := range tls.Certificates {
-		for _, reallyCertificate := range certificate.Certificate {
-			pemData = append(pemData, pem.EncodeToMemory(&pem.Block{
-				Type:  "CERTIFICATE",
-				Bytes: reallyCertificate,
-			})...)
-		}
-	}
-	return &auth1alpha1.TLSSpec{
-		CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemData),
-	}
-}
-
 func createJWT(
 	t *testing.T,
 	signingKey interface{},
@@ -1868,11 +1852,3 @@ func newCacheValue(t *testing.T, spec auth1alpha1.JWTAuthenticatorSpec, wantClos
 		spec:                     &spec,
 	}
 }
-
-func logLines(logs string) []string {
-	if len(logs) == 0 {
-		return nil
-	}
-
-	return strings.Split(strings.TrimSpace(logs), "\n")
-}
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index 191f7c77b..b3344914e 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 // Package webhookcachefiller implements a controller for filling an authncache.Cache with each added/updated WebhookAuthenticator.
@@ -6,6 +6,7 @@ package webhookcachefiller
 
 import (
 	"context"
+	"crypto/tls"
 	"crypto/x509"
 	"fmt"
 	"net/url"
@@ -33,15 +34,16 @@ import (
 	"go.pinniped.dev/internal/controller/authenticator/authncache"
 	"go.pinniped.dev/internal/controller/conditionsutil"
 	"go.pinniped.dev/internal/controllerlib"
+	"go.pinniped.dev/internal/endpointaddr"
 	"go.pinniped.dev/internal/plog"
 )
 
 const (
 	controllerName                   = "webhookcachefiller-controller"
 	typeReady                        = "Ready"
-	typeTLSConfigurationValid        = "TLSConfigurationValid"
+	typeTLSBundleValid               = "TLSBundleValid"
+	typeTLSConnetionNegotiationValid = "TLSConnetionNegotiationValid"
 	typeEndpointURLValid             = "EndpointURLValid"
-	typeEndpointPOSTValid            = "EndpointPOSTValid"
 	typeAuthenticatorValid           = "AuthenticatorValid"
 	reasonSuccess                    = "Success"
 	reasonNotReady                   = "NotReady"
@@ -53,7 +55,8 @@ const (
 	reasonInvalidTLSConfiguration    = "InvalidTLSConfiguration"
 	reasonInvalidEndpointURL         = "InvalidEndpointURL"
 	reasonInvalidEndpointURLScheme   = "InvalidEndpointURLScheme"
-	msgUnableToValidate              = "unable to validate; other issues present"
+	reasonUnableToDialServer         = "UnableToDialServer"
+	msgUnableToValidate              = "unable to validate; see other conditions for details"
 )
 
 // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
@@ -63,16 +66,18 @@ func New(
 	webhooks authinformers.WebhookAuthenticatorInformer,
 	clock clock.Clock,
 	log plog.Logger,
+	tlsDialerFunc func(network string, addr string, config *tls.Config) (*tls.Conn, error),
 ) controllerlib.Controller {
 	return controllerlib.New(
 		controllerlib.Config{
 			Name: controllerName,
 			Syncer: &webhookCacheFillerController{
-				cache:    cache,
-				client:   client,
-				webhooks: webhooks,
-				clock:    clock,
-				log:      log.WithName(controllerName),
+				cache:         cache,
+				client:        client,
+				webhooks:      webhooks,
+				clock:         clock,
+				log:           log.WithName(controllerName),
+				tlsDialerFunc: tlsDialerFunc,
 			},
 		},
 		controllerlib.WithInformer(
@@ -84,11 +89,12 @@ func New(
 }
 
 type webhookCacheFillerController struct {
-	cache    *authncache.Cache
-	webhooks authinformers.WebhookAuthenticatorInformer
-	client   conciergeclientset.Interface
-	clock    clock.Clock
-	log      plog.Logger
+	cache         *authncache.Cache
+	webhooks      authinformers.WebhookAuthenticatorInformer
+	client        conciergeclientset.Interface
+	clock         clock.Clock
+	log           plog.Logger
+	tlsDialerFunc func(network string, addr string, config *tls.Config) (*tls.Conn, error)
 }
 
 // Sync implements controllerlib.Syncer.
@@ -106,25 +112,31 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 	specCopy := obj.Spec.DeepCopy()
 	var errs []error
 
-	_, conditions, tlsOk := c.validateTLS(specCopy.TLS, conditions)
-	_, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
-	conditions, endpointPOSTOk := c.validateEndpointPOST(specCopy.Endpoint, conditions, tlsOk && endpointOk)
+	certPool, conditions, tlsBundleOk := c.validateTLSBundle(specCopy.TLS, conditions)
+	endpointURL, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
+	okSoFar := tlsBundleOk && endpointOk
+	conditions, tlsNegotiateErr := c.validateTLSNegotiation(certPool, endpointURL, conditions, okSoFar)
+	errs = append(errs, tlsNegotiateErr)
+	okSoFar = okSoFar && tlsNegotiateErr == nil
 
 	webhookAuthenticator, conditions, err := newWebhookAuthenticator(
 		&obj.Spec,
 		os.CreateTemp,
 		clientcmd.WriteToFile,
 		conditions,
-		tlsOk && endpointOk && endpointPOSTOk,
+		okSoFar,
 	)
 	errs = append(errs, err)
 
-	c.cache.Store(authncache.Key{
-		APIGroup: auth1alpha1.GroupName,
-		Kind:     "WebhookAuthenticator",
-		Name:     ctx.Key.Name,
-	}, webhookAuthenticator)
-	c.log.WithValues("webhook", klog.KObj(obj), "endpoint", obj.Spec.Endpoint).Info("added new webhook authenticator")
+	if !conditionsutil.HadErrorCondition(conditions) {
+		c.cache.Store(authncache.Key{
+			APIGroup: auth1alpha1.GroupName,
+			Kind:     "WebhookAuthenticator",
+			Name:     ctx.Key.Name,
+		}, webhookAuthenticator)
+		c.log.WithValues("webhook", klog.KObj(obj), "endpoint", obj.Spec.Endpoint).Info("added new webhook authenticator")
+	}
+
 	err = c.updateStatus(ctx.Context, obj, conditions)
 	errs = append(errs, err)
 
@@ -266,21 +278,76 @@ func newWebhookAuthenticator(
 	return webhookA, conditions, nil
 }
 
-func (c *webhookCacheFillerController) validateTLS(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
+func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.CertPool, endpointURL *url.URL, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
+	if !prereqOk {
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeTLSConnetionNegotiationValid,
+			Status:  metav1.ConditionUnknown,
+			Reason:  reasonUnableToValidate,
+			Message: msgUnableToValidate,
+		})
+		return conditions, nil
+	}
+
+	// dial requires domain, IPv4 or IPv6 w/o protocol
+	endpointHostPort, err := endpointaddr.Parse(endpointURL.Host, 443)
+	if err != nil {
+		// we have already validated the endpoint with url.Parse(endpoint) in c.validateEndpoint()
+		// so there is no reason to have a parsing error here.
+		c.log.Error("error parsing endpoint", err)
+	}
+
+	conn, dialErr := c.tlsDialerFunc("tcp", endpointHostPort.Endpoint(), &tls.Config{
+		MinVersion: tls.VersionTLS12,
+		// If certPool is nil then RootCAs will be set to nil and TLS will use the host's root CA set automatically.
+		RootCAs: certPool,
+	})
+
+	if dialErr != nil {
+		errText := "cannot dial server"
+		msg := fmt.Sprintf("%s: %s", errText, dialErr.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeTLSConnetionNegotiationValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonUnableToDialServer,
+			Message: msg,
+		})
+		return conditions, fmt.Errorf("%s: %w", errText, dialErr)
+	}
+
+	// this error should never be significant
+	err = conn.Close()
+	if err != nil {
+		c.log.Error("error closing dialer", err)
+	}
+
+	conditions = append(conditions, &metav1.Condition{
+		Type:    typeTLSConnetionNegotiationValid,
+		Status:  metav1.ConditionTrue,
+		Reason:  reasonSuccess,
+		Message: "tls verified",
+	})
+	return conditions, nil
+}
+
+func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
 	rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec)
 	if err != nil {
 		msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeTLSConfigurationValid,
+			Type:    typeTLSBundleValid,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonInvalidTLSConfiguration,
 			Message: msg,
 		})
 		return rootCAs, conditions, false
 	}
-	msg := "valid TLS configuration"
+	msg := "successfully parsed specified CA bundle"
+	if rootCAs == nil {
+		msg = "no CA bundle specified"
+	}
 	conditions = append(conditions, &metav1.Condition{
-		Type:    typeTLSConfigurationValid,
+		Type:    typeTLSBundleValid,
 		Status:  metav1.ConditionTrue,
 		Reason:  reasonSuccess,
 		Message: msg,
@@ -300,9 +367,9 @@ func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditi
 		})
 		return nil, conditions, false
 	}
-
+	// handles empty string and other issues as well.
 	if endpointURL.Scheme != "https" {
-		msg := fmt.Sprintf("spec.issuer %s has invalid scheme, require 'https'", endpoint)
+		msg := fmt.Sprintf("spec.endpoint %s has invalid scheme, require 'https'", endpoint)
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeEndpointURLValid,
 			Status:  metav1.ConditionFalse,
@@ -321,31 +388,11 @@ func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditi
 	return endpointURL, conditions, true
 }
 
-func (c *webhookCacheFillerController) validateEndpointPOST(endpoint string, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, bool) {
-	if endpoint == "" {
-		// TODO(BEN): do something with this. time to validate the endpoint will receive a POST
-		fmt.Println("FIX THIS")
-	}
-	if !prereqOk {
-		conditions = append(conditions, &metav1.Condition{
-			Type:    typeEndpointPOSTValid,
-			Status:  metav1.ConditionUnknown,
-			Reason:  reasonUnableToValidate,
-			Message: msgUnableToValidate,
-		})
-		return conditions, false
-	}
-
-	// TODO: do some things here so this func makes sense.
-	return conditions, false
-}
-
 func (c *webhookCacheFillerController) updateStatus(
 	ctx context.Context,
 	original *auth1alpha1.WebhookAuthenticator,
 	conditions []*metav1.Condition,
 ) error {
-
 	updated := original.DeepCopy()
 
 	if hadErrorCondition(conditions) {
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index be5e369dd..c18ba7a2e 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package webhookcachefiller
@@ -6,17 +6,27 @@ package webhookcachefiller
 import (
 	"bytes"
 	"context"
+	"crypto/tls"
 	"encoding/base64"
+	"encoding/json"
+	"errors"
 	"fmt"
 	"io"
+	"net"
 	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"os"
 	"testing"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	coretesting "k8s.io/client-go/testing"
 	"k8s.io/client-go/tools/clientcmd"
 	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 	clocktesting "k8s.io/utils/clock/testing"
@@ -24,23 +34,171 @@ import (
 	auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 	pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
 	pinnipedinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions"
+	"go.pinniped.dev/internal/certauthority"
 	"go.pinniped.dev/internal/controller/authenticator/authncache"
 	"go.pinniped.dev/internal/controllerlib"
+	"go.pinniped.dev/internal/crypto/ptls"
 	"go.pinniped.dev/internal/plog"
 	"go.pinniped.dev/internal/testutil"
+	"go.pinniped.dev/internal/testutil/conciergetestutil"
 	"go.pinniped.dev/internal/testutil/conditionstestutil"
 	"go.pinniped.dev/internal/testutil/testlogger"
+	"go.pinniped.dev/internal/testutil/tlsserver"
 )
 
 func TestController(t *testing.T) {
 	t.Parallel()
 
-	goodEndpoint := "https://example.com"
+	caForLocalhostAsHostname, err := certauthority.New("My Localhost CA Common Name", time.Hour)
+	require.NoError(t, err)
+	onlyLocalhostAsHost := []string{"localhost"}
+	noIPAddressesNotEven127001 := []net.IP{}
+	hostAsLocalhostServingCert, err := caForLocalhostAsHostname.IssueServerCert(
+		onlyLocalhostAsHost,
+		noIPAddressesNotEven127001,
+		time.Hour,
+	)
+	require.NoError(t, err)
+
+	caForLocalhostAs127001, err := certauthority.New("My Localhost CA Common Name", time.Hour)
+	require.NoError(t, err)
+	noHostnameHost := []string{}
+	only127001IPAddress := []net.IP{net.ParseIP("127.0.0.1")}
+	hostAs127001ServingCert, err := caForLocalhostAs127001.IssueServerCert(
+		noHostnameHost,
+		only127001IPAddress,
+		time.Hour,
+	)
+	require.NoError(t, err)
+
+	caForUnknownServer, err := certauthority.New("Some Unknown CA", time.Hour)
+	require.NoError(t, err)
+	someUnknownHostNames := []string{"some-dns-name", "some-other-dns-name"}
+	someLocalIPAddress := []net.IP{net.ParseIP("10.2.3.4")}
+	pemServerCertForUnknownServer, _, err := caForUnknownServer.IssueServerCertPEM(
+		someUnknownHostNames,
+		someLocalIPAddress,
+		time.Hour,
+	)
+	require.NoError(t, err)
+
+	caForExampleDotCom, err := certauthority.New("Some Example.com CA", time.Hour)
+	require.NoError(t, err)
+	exampleDotComHostname := []string{"example.com"}
+	localButExampleDotComServerCert, err := caForExampleDotCom.IssueServerCert(
+		exampleDotComHostname,
+		[]net.IP{},
+		time.Hour,
+	)
+	require.NoError(t, err)
+
+	hostAsLocalhostMux := http.NewServeMux()
+	hostAsLocalhostWebhookServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tlsserver.AssertTLS(t, r, ptls.Default)
+		hostAsLocalhostMux.ServeHTTP(w, r)
+	}), func(thisServer *httptest.Server) {
+		thisTLSConfig := ptls.Default(nil)
+		thisTLSConfig.Certificates = []tls.Certificate{
+			// public and private key pair, but server will only use private for serving
+			*hostAsLocalhostServingCert,
+		}
+		thisServer.TLS = thisTLSConfig
+	})
+
+	hostAs127001Mux := http.NewServeMux()
+	hostAs127001WebhookServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tlsserver.AssertTLS(t, r, ptls.Default)
+		hostAs127001Mux.ServeHTTP(w, r)
+	}), func(thisServer *httptest.Server) {
+		thisTLSConfig := ptls.Default(nil)
+		thisTLSConfig.Certificates = []tls.Certificate{
+			*hostAs127001ServingCert,
+		}
+		thisServer.TLS = thisTLSConfig
+	})
+
+	localWithExampleDotComMux := http.NewServeMux()
+	localWithExampleDotComCertServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tlsserver.AssertTLS(t, r, ptls.Default)
+		localWithExampleDotComMux.ServeHTTP(w, r)
+	}), func(thisServer *httptest.Server) {
+		thisTLSConfig := ptls.Default(nil)
+		thisTLSConfig.Certificates = []tls.Certificate{
+			*localButExampleDotComServerCert,
+		}
+		thisServer.TLS = thisTLSConfig
+	})
+
+	goodMux := http.NewServeMux()
+	goodWebhookServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		tlsserver.AssertTLS(t, r, ptls.Default)
+		goodMux.ServeHTTP(w, r)
+	}), tlsserver.RecordTLSHello)
+	goodMux.Handle("/some/webhook", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		_, err := fmt.Fprintf(w, `{"something": "%s"}`, "something-for-response")
+		require.NoError(t, err)
+	}))
+	goodMux.Handle("/nothing/here", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusNotFound)
+		fmt.Fprint(w, "404 nothing here")
+	}))
+	goodEndpoint := goodWebhookServer.URL
+	goodEndpointBut404 := goodEndpoint + "/nothing/here"
+
+	localhostURL, err := url.Parse(hostAsLocalhostWebhookServer.URL)
+	require.NoError(t, err)
+	localhostEndpointURL := fmt.Sprintf("%s:%s", "https://localhost", localhostURL.Port())
+
+	badEndpoint := "https://.café   .com/café/café/café/coffee"
+	badEndpointNoHTTPS := "http://localhost"
 
 	nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local)
 	frozenMetav1Now := metav1.NewTime(nowDoesntMatter)
 	frozenClock := clocktesting.NewFakeClock(nowDoesntMatter)
 
+	timeInThePast := time.Date(1111, time.January, 1, 1, 1, 1, 111111, time.Local)
+	frozenTimeInThePast := metav1.NewTime(timeInThePast)
+
+	goodWebhookAuthenticatorSpecWithCA := auth1alpha1.WebhookAuthenticatorSpec{
+		Endpoint: goodEndpoint,
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodWebhookServer.TLS),
+	}
+	localhostWebhookAuthenticatorSpecWithCA := auth1alpha1.WebhookAuthenticatorSpec{
+		Endpoint: localhostEndpointURL,
+		TLS: &auth1alpha1.TLSSpec{
+			// CA Bundle for validating the server's certs
+			CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAsHostname.Bundle()),
+		},
+	}
+	localWithExampleDotComWeebhookAuthenticatorSpec := auth1alpha1.WebhookAuthenticatorSpec{
+		// CA for example.com, TLS serving cert for example.com, but endpoint is still localhost
+		Endpoint: localWithExampleDotComCertServer.URL,
+		TLS: &auth1alpha1.TLSSpec{
+			// CA Bundle for example.com
+			CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForExampleDotCom.Bundle()),
+		},
+	}
+	goodWebhookAuthenticatorSpecWithoutCA := auth1alpha1.WebhookAuthenticatorSpec{
+		Endpoint: goodEndpoint,
+		TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: ""},
+	}
+	goodWebhookAuthenticatorSpecWith404Endpoint := auth1alpha1.WebhookAuthenticatorSpec{
+		Endpoint: goodEndpointBut404,
+		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodWebhookServer.TLS),
+	}
+	badWebhookAuthenticatorSpecInvalidTLS := auth1alpha1.WebhookAuthenticatorSpec{
+		Endpoint: goodEndpoint,
+		TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"},
+	}
+
+	badWebhookAuthenticatorSpecGoodEndpointButUnknownCA := auth1alpha1.WebhookAuthenticatorSpec{
+		Endpoint: goodEndpoint,
+		TLS: &auth1alpha1.TLSSpec{
+			CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemServerCertForUnknownServer),
+		},
+	}
+
 	happyReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
 			Type:               "Ready",
@@ -51,16 +209,16 @@ func TestController(t *testing.T) {
 			Message:            "the WebhookAuthenticator is ready",
 		}
 	}
-	// sadReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
-	// 	return metav1.Condition{
-	// 		Type:               "Ready",
-	// 		Status:             "False",
-	// 		ObservedGeneration: observedGeneration,
-	// 		LastTransitionTime: time,
-	// 		Reason:             "NotReady",
-	// 		Message:            "the WebhookAuthenticator is not ready: see other conditions for details",
-	// 	}
-	// }
+	sadReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "Ready",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "NotReady",
+			Message:            "the WebhookAuthenticator is not ready: see other conditions for details",
+		}
+	}
 	happyAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
 			Type:               "AuthenticatorValid",
@@ -71,38 +229,88 @@ func TestController(t *testing.T) {
 			Message:            "authenticator initialized",
 		}
 	}
-	// unknownAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
-	// 	return metav1.Condition{
-	// 		Type:               "AuthenticatorValid",
-	// 		Status:             "Unknown",
-	// 		ObservedGeneration: observedGeneration,
-	// 		LastTransitionTime: time,
-	// 		Reason:             "UnableToValidate",
-	// 		Message:            "unable to validate; other issues present",
-	// 	}
-	// }
-	// sadAuthenticatorValid := func() metav1.Condition {}
-
-	happyTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	unknownAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConfigurationValid",
+			Type:               "AuthenticatorValid",
+			Status:             "Unknown",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "UnableToValidate",
+			Message:            "unable to validate; see other conditions for details",
+		}
+	}
+
+	happyTLSBundleValidCAParsed := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSBundleValid",
 			Status:             "True",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
 			Reason:             "Success",
-			Message:            "valid TLS configuration",
+			Message:            "successfully parsed specified CA bundle",
+		}
+	}
+	happyTLSBundleValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSBundleValid",
+			Status:             "True",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "Success",
+			Message:            "no CA bundle specified",
+		}
+	}
+	sadTLSBundleValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSBundleValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidTLSConfiguration",
+			Message:            "invalid TLS configuration: illegal base64 data at input byte 7",
+		}
+	}
+
+	happyTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSConnetionNegotiationValid",
+			Status:             "True",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "Success",
+			Message:            "tls verified",
+		}
+	}
+	unknownTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSConnetionNegotiationValid",
+			Status:             "Unknown",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "UnableToValidate",
+			Message:            "unable to validate; see other conditions for details",
+		}
+	}
+	sadTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSConnetionNegotiationValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "UnableToDialServer",
+			Message:            "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
+		}
+	}
+	sadTLSConnetionNegotiationNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "TLSConnetionNegotiationValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "UnableToDialServer",
+			Message:            "cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs",
 		}
 	}
-	// sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
-	// 	return metav1.Condition{
-	// 		Type:               "TLSConfigurationValid",
-	// 		Status:             "False",
-	// 		ObservedGeneration: observedGeneration,
-	// 		LastTransitionTime: time,
-	// 		Reason:             "InvalidTLSConfiguration",
-	// 		Message:            "invalid TLS configuration: illegal base64 data at input byte 7",
-	// 	}
-	// }
 
 	happyEndpointURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
@@ -114,73 +322,500 @@ func TestController(t *testing.T) {
 			Message:            "endpoint is a valid URL",
 		}
 	}
-	// happyEndpointURLValidInvalid := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
-	// 	return metav1.Condition{
-	// 		Type:               "EndpointURLValid",
-	// 		Status:             "False",
-	// 		ObservedGeneration: observedGeneration,
-	// 		LastTransitionTime: time,
-	// 		Reason:             "InvalidIssuerURL",
-	// 		Message:            fmt.Sprintf(`spec.endpoint URL is invalid: parse "%s": invalid character " " in host name`, issuer),
-	// 	}
-	// }
+	sadEndpointURLValid := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "EndpointURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidEndpointURL",
+			Message:            fmt.Sprintf(`spec.endpoint URL is invalid: parse "%s": invalid character " " in host name`, issuer),
+		}
+	}
+	sadEndpointURLValidHTTPS := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "EndpointURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidEndpointURLScheme",
+			Message:            fmt.Sprintf(`spec.endpoint %s has invalid scheme, require 'https'`, issuer),
+		}
+	}
 
 	allHappyConditionsSuccess := func(endpoint string, someTime metav1.Time, observedGeneration int64) []metav1.Condition {
-
 		return conditionstestutil.SortByType([]metav1.Condition{
+			happyTLSBundleValidCAParsed(someTime, observedGeneration),
 			happyEndpointURLValid(someTime, observedGeneration),
+			happyTLSConnetionNegotiationValid(someTime, observedGeneration),
 			happyAuthenticatorValid(someTime, observedGeneration),
 			happyReadyCondition(someTime, observedGeneration),
-			happyTLSConfigurationValid(someTime, observedGeneration),
 		})
 	}
 
+	webhookAuthenticatorGVR := schema.GroupVersionResource{
+		Group:    "authentication.concierge.pinniped.dev",
+		Version:  "v1alpha1",
+		Resource: "webhookauthenticators",
+	}
+	webhookAuthenticatorGVK := schema.GroupVersionKind{
+		Group:   "authentication.concierge.pinniped.dev",
+		Version: "v1alpha1",
+		Kind:    "WebhookAuthenticator",
+	}
+
 	tests := []struct {
-		name                 string
-		syncKey              controllerlib.Key
-		webhooks             []runtime.Object
-		wantErr              string
-		wantLogs             []string
-		wantStatusConditions []metav1.Condition
-		wantStatusPhase      auth1alpha1.WebhookAuthenticatorPhase
-		wantCacheEntries     int
+		name             string
+		syncKey          controllerlib.Key
+		webhooks         []runtime.Object
+		tlsDialerFunc    func(network string, addr string, config *tls.Config) (*tls.Conn, error)
+		wantSyncLoopErr  testutil.RequireErrorStringFunc
+		wantLogs         []map[string]any
+		wantActions      func() []coretesting.Action
+		wantCacheEntries int
 	}{
 		{
-			name:    "404: webhook authenticator not found will abort sync loop and not write status",
+			name:    "404: WebhookAuthenticator not found will abort sync loop, no status conditions",
 			syncKey: controllerlib.Key{Name: "test-name"},
-			// TODO(BEN): we lost this line when swapping loggers. Is that ok?
-			//   did the JWTAuthenticator also lose it?  Should we ensure something exists otherwise?
-			// wantLogs: []string{
-			// 	`webhookcachefiller-controller "level"=0 "msg"="Sync() found that the WebhookAuthenticator does not exist yet or was deleted"`,
-			// },
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "Sync() found that the WebhookAuthenticator does not exist yet or was deleted",
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+				}
+			},
+			wantCacheEntries: 0,
 		},
 		// Existing code that was never tested. We would likely have to create a server with bad clients to
 		// simulate this.
 		// { name: "non-404 `failed to get webhook authenticator` for other API server reasons" }
 		{
-			//  will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error
-			name:    "invalid webhook will fail the sync loop and........????",
+			name:    "Sync: valid and unchanged WebhookAuthenticator: loop will preserve existing status conditions",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "test-name",
 					},
-					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: "invalid url",
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodWebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "Sync: changed WebhookAuthenticator: loop will update timestamps only on relevant statuses",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadReadyCondition(frozenTimeInThePast, 0),
+								happyEndpointURLValid(frozenTimeInThePast, 0),
+							},
+						),
+						Phase: "Ready",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodWebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								happyEndpointURLValid(frozenTimeInThePast, 0),
+							},
+						),
+						Phase: "Ready",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "Sync: valid WebhookAuthenticator with CA: will complete sync loop successfully with success conditions and ready phase",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodWebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
 					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 1,
+		},
+		{
+			name:    "Sync: valid WebhookAuthenticator without CA: loop will fail to cache the authenticator, will write failed and unknown status conditions, and will enqueue resync",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithoutCA,
 				},
 			},
-			wantErr: `failed to build webhook config: parse "http://invalid url": invalid character " " in host name`,
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithoutCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								happyTLSBundleValidNoCA(frozenMetav1Now, 0),
+								sadTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority`),
+			wantCacheEntries: 0,
 		},
-		// TODO (BEN): add valid without CA?
 		{
-			name: "valid webhook without CA...",
+			name:    "validateTLS: WebhookAuthenticator with invalid CA will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: badWebhookAuthenticatorSpecInvalidTLS,
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: badWebhookAuthenticatorSpecInvalidTLS,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadTLSBundleValid(frozenMetav1Now, 0),
+								unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 0,
+		},
+		{
+			name:    "validateEndpoint: parsing error (spec.endpoint URL is invalid) will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: badEndpoint,
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: badEndpoint,
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								happyTLSBundleValidNoCA(frozenMetav1Now, 0),
+								sadEndpointURLValid("https://.café   .com/café/café/café/coffee", frozenMetav1Now, 0),
+								unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 0,
 		}, {
-			name: "",
+			name:    "validateEndpoint: parsing error (spec.endpoint URL has invalid scheme, requires https) will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: badEndpointNoHTTPS,
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: badEndpointNoHTTPS,
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								happyTLSBundleValidNoCA(frozenMetav1Now, 0),
+								sadEndpointURLValidHTTPS("http://localhost", frozenMetav1Now, 0),
+								unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 0,
 		},
 		{
-			name:    "valid webhook will complete sync loop successfully with success conditions and ready phase",
+			name:    "validateTLSNegotiation: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: badWebhookAuthenticatorSpecGoodEndpointButUnknownCA,
+				},
+			},
+			wantSyncLoopErr: testutil.WantExactErrorString("cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority"),
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: badWebhookAuthenticatorSpecGoodEndpointButUnknownCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+								sadTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 0,
+		},
+		// No unit test for system roots.  We don't test the JWTAuthenticator's use of system roots either.
+		// We would have to find a way to mock out roots by adding a dummy cert in order to test this
+		// { name: "validateTLSNegotiation: TLS bundle not provided should use system roots to validate server cert signed by a well-known CA",},
+		{
+			name:    "validateTLSNegotiation: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWith404Endpoint,
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodEndpointBut404,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWith404Endpoint,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(goodEndpointBut404, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "validateTLSNegotiation: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: localhostWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(localhostEndpointURL, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  localhostEndpointURL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "validateTLSNegotiation: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -188,18 +823,78 @@ func TestController(t *testing.T) {
 						Name: "test-name",
 					},
 					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: goodEndpoint,
-						TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: ""},
+						Endpoint: hostAs127001WebhookServer.URL,
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(hostAs127001WebhookServer.URL, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  hostAs127001WebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "validateTLSNegotiation: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: localWithExampleDotComWeebhookAuthenticatorSpec,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(localWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
+						Phase:      "Ready",
 					},
 				},
 			},
-			// TODO(BEN): we lost this changing loggers, make sure its captured in conditions
-			// wantLogs: []string{
-			// 	`webhookcachefiller-controller "level"=0 "msg"="added new webhook authenticator" "endpoint"="https://example.com" "webhook"={"name":"test-name"}`,
-			// },
-			wantStatusConditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
-			wantStatusPhase:      "Ready",
-			wantCacheEntries:     1,
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: localWithExampleDotComWeebhookAuthenticatorSpec,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(localWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadTLSConnetionNegotiationNoIPSANs(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 0,
+			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`),
 		},
 	}
 	for _, tt := range tests {
@@ -210,17 +905,20 @@ func TestController(t *testing.T) {
 			pinnipedAPIClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
 			informers := pinnipedinformers.NewSharedInformerFactory(pinnipedAPIClient, 0)
 			cache := authncache.New()
-			testLog := testlogger.NewLegacy(t) //nolint:staticcheck  // old test with lots of log statements
 
 			var log bytes.Buffer
 			logger := plog.TestLogger(t, &log)
 
+			if tt.tlsDialerFunc == nil {
+				tt.tlsDialerFunc = tls.Dial
+			}
 			controller := New(
 				cache,
 				pinnipedAPIClient,
 				informers.Authentication().V1alpha1().WebhookAuthenticators(),
 				frozenClock,
-				logger)
+				logger,
+				tt.tlsDialerFunc)
 
 			ctx, cancel := context.WithCancel(context.Background())
 			defer cancel()
@@ -230,21 +928,42 @@ func TestController(t *testing.T) {
 
 			syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey}
 
-			if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" {
-				require.EqualError(t, err, tt.wantErr)
+			if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantSyncLoopErr != nil {
+				testutil.RequireErrorStringFromErr(t, err, tt.wantSyncLoopErr)
 			} else {
 				require.NoError(t, err)
 			}
-			require.Equal(t, tt.wantLogs, testLog.Lines(), "log lines should be correct")
-
-			if tt.webhooks != nil {
-				var webhookAuthSubject *auth1alpha1.WebhookAuthenticator
-				getCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
-				defer cancel()
-				webhookAuthSubject, getErr := pinnipedAPIClient.AuthenticationV1alpha1().WebhookAuthenticators().Get(getCtx, "test-name", metav1.GetOptions{})
-				require.NoError(t, getErr)
-				require.Equal(t, tt.wantStatusConditions, webhookAuthSubject.Status.Conditions, "status.conditions must be correct")
-				require.Equal(t, tt.wantStatusPhase, webhookAuthSubject.Status.Phase, "status.phase should be correct")
+			actualLogLines := testlogger.LogLines(log.String())
+			require.Equal(t, len(actualLogLines), len(tt.wantLogs), "log line count should be correct")
+
+			for logLineNum, logLine := range actualLogLines {
+				require.NotNil(t, tt.wantLogs[logLineNum], "expected log line should never be empty")
+				var lineStruct map[string]any
+				err := json.Unmarshal([]byte(logLine), &lineStruct)
+				require.NoError(t, err)
+				require.Equal(t, tt.wantLogs[logLineNum]["level"], lineStruct["level"], fmt.Sprintf("log line (%d) log level should be correct (in: %s)", logLineNum, lineStruct))
+
+				require.Equal(t, tt.wantLogs[logLineNum]["timestamp"], lineStruct["timestamp"], fmt.Sprintf("log line (%d) timestamp should be correct (in: %s)", logLineNum, lineStruct))
+				require.Equal(t, lineStruct["logger"], tt.wantLogs[logLineNum]["logger"], fmt.Sprintf("log line (%d) logger should be correct", logLineNum))
+				require.NotEmpty(t, lineStruct["caller"], fmt.Sprintf("log line (%d) caller should not be empty", logLineNum))
+				require.Equal(t, tt.wantLogs[logLineNum]["message"], lineStruct["message"], fmt.Sprintf("log line (%d) message should be correct", logLineNum))
+				if lineStruct["webhook"] != nil {
+					require.Equal(t, tt.wantLogs[logLineNum]["webhook"], lineStruct["webhook"], fmt.Sprintf("log line (%d) webhook should be correct", logLineNum))
+				}
+				if lineStruct["endpoint"] != nil {
+					require.Equal(t, tt.wantLogs[logLineNum]["endpoint"], lineStruct["endpoint"], fmt.Sprintf("log line (%d) endpoint should be correct", logLineNum))
+				}
+			}
+
+			if tt.wantActions != nil {
+				if !assert.ElementsMatch(t, tt.wantActions(), pinnipedAPIClient.Actions()) {
+					// cmp.Diff is superior to require.ElementsMatch in terms of readability here.
+					// require.ElementsMatch will handle pointers better than require.Equal, but
+					// the timestamps are still incredibly verbose.
+					require.Fail(t, cmp.Diff(tt.wantActions(), pinnipedAPIClient.Actions()), "actions should be exactly the expected number of actions and also contain the correct resources")
+				}
+			} else {
+				require.Error(t, errors.New("wantActions is required for test "+tt.name))
 			}
 
 			require.Equal(t, tt.wantCacheEntries, len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", tt.wantCacheEntries, len(cache.Keys()), cache.Keys()))
@@ -263,7 +982,7 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 				Type:    "AuthenticatorValid",
 				Status:  "Unknown",
 				Reason:  "UnableToValidate",
-				Message: "unable to validate; other issues present",
+				Message: "unable to validate; see other conditions for details",
 			},
 		}, conditions)
 		require.Nil(t, res)
@@ -302,7 +1021,7 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 		require.EqualError(t, err, "unable to marshal kubeconfig: some marshal error")
 	})
 
-	// t.Run("load kubeconfig err, not currently tested, may not be reasonable to test?")
+	// t.Run("load kubeconfig err, not currently tested, may not be necessary to test?")
 
 	t.Run("invalid TLS config, base64 encoding err, cannot create webhook authenticator", func(t *testing.T) {
 		conditions := []*metav1.Condition{}
diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go
index 379db68a4..3a65c7caf 100644
--- a/internal/controllermanager/prepare_controllers.go
+++ b/internal/controllermanager/prepare_controllers.go
@@ -6,6 +6,7 @@
 package controllermanager
 
 import (
+	"crypto/tls"
 	"fmt"
 	"time"
 
@@ -240,6 +241,7 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol
 				informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(),
 				clock.RealClock{},
 				plog.New(),
+				tls.Dial,
 			),
 			singletonWorker,
 		).
diff --git a/internal/testutil/testlogger/loglines.go b/internal/testutil/testlogger/loglines.go
new file mode 100644
index 000000000..8ec15f320
--- /dev/null
+++ b/internal/testutil/testlogger/loglines.go
@@ -0,0 +1,14 @@
+// Copyright 2024 the Pinniped contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package testlogger
+
+import "strings"
+
+func LogLines(logs string) []string {
+	if len(logs) == 0 {
+		return nil
+	}
+
+	return strings.Split(strings.TrimSpace(logs), "\n")
+}
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index 099848b22..558217660 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -2,3 +2,9 @@
 // SPDX-License-Identifier: Apache-2.0
 
 package integration
+
+// TODO: for integration tests, not unit tests....
+// env.APIGroupSuffix for log messages/conditions when relevant, ie if "pinniped.dev" appears
+// env.CLIUpstreamOIDC.Issuer if a real endpoint is needed as we shouldn't actually make requests
+//   to example.com
+// goodEndpoint := "https://example.com"

From 337459feb0c99c9d36835537fcfdb3546cf69a94 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Wed, 13 Mar 2024 10:55:02 -0400
Subject: [PATCH 03/17] Update webhook status integration tests

- total api fields test 260->261
---
 .../webhookcachefiller/webhookcachefiller.go  |  46 +--
 .../webhookcachefiller_test.go                |  52 +--
 .../testutil/conciergetestutil/tlstestutil.go |  28 ++
 test/integration/cli_test.go                  |   5 +-
 .../concierge_api_serving_certs_test.go       |   5 +-
 test/integration/concierge_client_test.go     |   5 +-
 .../concierge_credentialrequest_test.go       |  15 +-
 .../concierge_impersonation_proxy_test.go     |   3 +-
 ...cierge_webhookauthenticator_status_test.go | 296 +++++++++++++++++-
 test/integration/kube_api_discovery_test.go   |   2 +-
 test/testlib/client.go                        |  58 +++-
 11 files changed, 446 insertions(+), 69 deletions(-)
 create mode 100644 internal/testutil/conciergetestutil/tlstestutil.go

diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index b3344914e..592e04e9b 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -39,24 +39,24 @@ import (
 )
 
 const (
-	controllerName                   = "webhookcachefiller-controller"
-	typeReady                        = "Ready"
-	typeTLSBundleValid               = "TLSBundleValid"
-	typeTLSConnetionNegotiationValid = "TLSConnetionNegotiationValid"
-	typeEndpointURLValid             = "EndpointURLValid"
-	typeAuthenticatorValid           = "AuthenticatorValid"
-	reasonSuccess                    = "Success"
-	reasonNotReady                   = "NotReady"
-	reasonUnableToValidate           = "UnableToValidate"
-	reasonUnableToCreateTempFile     = "UnableToCreateTempFile"
-	reasonUnableToMarshallKubeconfig = "UnableToMarshallKubeconfig"
-	reasonUnableToLoadKubeconfig     = "UnableToLoadKubeconfig"
-	reasonUnableToInstantiateWebhook = "UnableToInstantiateWebhook"
-	reasonInvalidTLSConfiguration    = "InvalidTLSConfiguration"
-	reasonInvalidEndpointURL         = "InvalidEndpointURL"
-	reasonInvalidEndpointURLScheme   = "InvalidEndpointURLScheme"
-	reasonUnableToDialServer         = "UnableToDialServer"
-	msgUnableToValidate              = "unable to validate; see other conditions for details"
+	controllerName                    = "webhookcachefiller-controller"
+	typeReady                         = "Ready"
+	typeTLSConfigurationValid         = "TLSConfigurationValid"
+	typeTLSConnectionNegotiationValid = "TLSConnectionNegotiationValid"
+	typeEndpointURLValid              = "EndpointURLValid"
+	typeAuthenticatorValid            = "AuthenticatorValid"
+	reasonSuccess                     = "Success"
+	reasonNotReady                    = "NotReady"
+	reasonUnableToValidate            = "UnableToValidate"
+	reasonUnableToCreateTempFile      = "UnableToCreateTempFile"
+	reasonUnableToMarshallKubeconfig  = "UnableToMarshallKubeconfig"
+	reasonUnableToLoadKubeconfig      = "UnableToLoadKubeconfig"
+	reasonUnableToInstantiateWebhook  = "UnableToInstantiateWebhook"
+	reasonInvalidTLSConfiguration     = "InvalidTLSConfiguration"
+	reasonInvalidEndpointURL          = "InvalidEndpointURL"
+	reasonInvalidEndpointURLScheme    = "InvalidEndpointURLScheme"
+	reasonUnableToDialServer          = "UnableToDialServer"
+	msgUnableToValidate               = "unable to validate; see other conditions for details"
 )
 
 // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
@@ -281,7 +281,7 @@ func newWebhookAuthenticator(
 func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.CertPool, endpointURL *url.URL, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
 	if !prereqOk {
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeTLSConnetionNegotiationValid,
+			Type:    typeTLSConnectionNegotiationValid,
 			Status:  metav1.ConditionUnknown,
 			Reason:  reasonUnableToValidate,
 			Message: msgUnableToValidate,
@@ -307,7 +307,7 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer
 		errText := "cannot dial server"
 		msg := fmt.Sprintf("%s: %s", errText, dialErr.Error())
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeTLSConnetionNegotiationValid,
+			Type:    typeTLSConnectionNegotiationValid,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonUnableToDialServer,
 			Message: msg,
@@ -322,7 +322,7 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer
 	}
 
 	conditions = append(conditions, &metav1.Condition{
-		Type:    typeTLSConnetionNegotiationValid,
+		Type:    typeTLSConnectionNegotiationValid,
 		Status:  metav1.ConditionTrue,
 		Reason:  reasonSuccess,
 		Message: "tls verified",
@@ -335,7 +335,7 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL
 	if err != nil {
 		msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeTLSBundleValid,
+			Type:    typeTLSConfigurationValid,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonInvalidTLSConfiguration,
 			Message: msg,
@@ -347,7 +347,7 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL
 		msg = "no CA bundle specified"
 	}
 	conditions = append(conditions, &metav1.Condition{
-		Type:    typeTLSBundleValid,
+		Type:    typeTLSConfigurationValid,
 		Status:  metav1.ConditionTrue,
 		Reason:  reasonSuccess,
 		Message: msg,
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index c18ba7a2e..e21e7b45c 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -240,9 +240,9 @@ func TestController(t *testing.T) {
 		}
 	}
 
-	happyTLSBundleValidCAParsed := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	happyTLSConfigurationValidCAParsed := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSBundleValid",
+			Type:               "TLSConfigurationValid",
 			Status:             "True",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -250,9 +250,9 @@ func TestController(t *testing.T) {
 			Message:            "successfully parsed specified CA bundle",
 		}
 	}
-	happyTLSBundleValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	happyTLSConfigurationValidNoCA := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSBundleValid",
+			Type:               "TLSConfigurationValid",
 			Status:             "True",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -260,9 +260,9 @@ func TestController(t *testing.T) {
 			Message:            "no CA bundle specified",
 		}
 	}
-	sadTLSBundleValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSBundleValid",
+			Type:               "TLSConfigurationValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -271,9 +271,9 @@ func TestController(t *testing.T) {
 		}
 	}
 
-	happyTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	happyTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnetionNegotiationValid",
+			Type:               "TLSConnectionNegotiationValid",
 			Status:             "True",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -281,9 +281,9 @@ func TestController(t *testing.T) {
 			Message:            "tls verified",
 		}
 	}
-	unknownTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	unknownTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnetionNegotiationValid",
+			Type:               "TLSConnectionNegotiationValid",
 			Status:             "Unknown",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -291,9 +291,9 @@ func TestController(t *testing.T) {
 			Message:            "unable to validate; see other conditions for details",
 		}
 	}
-	sadTLSConnetionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnetionNegotiationValid",
+			Type:               "TLSConnectionNegotiationValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -301,9 +301,9 @@ func TestController(t *testing.T) {
 			Message:            "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
 		}
 	}
-	sadTLSConnetionNegotiationNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadTLSConnectionNegotiationNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnetionNegotiationValid",
+			Type:               "TLSConnectionNegotiationValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -345,9 +345,9 @@ func TestController(t *testing.T) {
 
 	allHappyConditionsSuccess := func(endpoint string, someTime metav1.Time, observedGeneration int64) []metav1.Condition {
 		return conditionstestutil.SortByType([]metav1.Condition{
-			happyTLSBundleValidCAParsed(someTime, observedGeneration),
+			happyTLSConfigurationValidCAParsed(someTime, observedGeneration),
 			happyEndpointURLValid(someTime, observedGeneration),
-			happyTLSConnetionNegotiationValid(someTime, observedGeneration),
+			happyTLSConnectionNegotiationValid(someTime, observedGeneration),
 			happyAuthenticatorValid(someTime, observedGeneration),
 			happyReadyCondition(someTime, observedGeneration),
 		})
@@ -551,8 +551,8 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								happyTLSBundleValidNoCA(frozenMetav1Now, 0),
-								sadTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
+								sadTLSConnectionNegotiationValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
@@ -591,8 +591,8 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadTLSBundleValid(frozenMetav1Now, 0),
-								unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								sadTLSConfigurationValid(frozenMetav1Now, 0),
+								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -634,9 +634,9 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								happyTLSBundleValidNoCA(frozenMetav1Now, 0),
+								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValid("https://.café   .com/café/café/café/coffee", frozenMetav1Now, 0),
-								unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -677,9 +677,9 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								happyTLSBundleValidNoCA(frozenMetav1Now, 0),
+								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValidHTTPS("http://localhost", frozenMetav1Now, 0),
-								unknownTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -720,7 +720,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
-								sadTLSConnetionNegotiationValid(frozenMetav1Now, 0),
+								sadTLSConnectionNegotiationValid(frozenMetav1Now, 0),
 							},
 						),
 						Phase: "Error",
@@ -878,7 +878,7 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(localWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadTLSConnetionNegotiationNoIPSANs(frozenMetav1Now, 0),
+								sadTLSConnectionNegotiationNoIPSANs(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
diff --git a/internal/testutil/conciergetestutil/tlstestutil.go b/internal/testutil/conciergetestutil/tlstestutil.go
new file mode 100644
index 000000000..0dc412e61
--- /dev/null
+++ b/internal/testutil/conciergetestutil/tlstestutil.go
@@ -0,0 +1,28 @@
+// Copyright 2024 the Pinniped contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package conciergetestutil
+
+import (
+	"crypto/tls"
+	"encoding/base64"
+	"encoding/pem"
+
+	auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
+)
+
+func TlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
+	pemData := make([]byte, 0)
+	for _, certificate := range tls.Certificates {
+		// this is the public part of the certificate, the private is the certificate.PrivateKey
+		for _, reallyCertificate := range certificate.Certificate {
+			pemData = append(pemData, pem.EncodeToMemory(&pem.Block{
+				Type:  "CERTIFICATE",
+				Bytes: reallyCertificate,
+			})...)
+		}
+	}
+	return &auth1alpha1.TLSSpec{
+		CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemData),
+	}
+}
diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go
index 968b7d065..394424da2 100644
--- a/test/integration/cli_test.go
+++ b/test/integration/cli_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 package integration
 
@@ -26,6 +26,7 @@ import (
 	"k8s.io/apimachinery/pkg/runtime/serializer"
 	clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
 
+	"go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 	identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
 	conciergescheme "go.pinniped.dev/internal/concierge/scheme"
 	"go.pinniped.dev/pkg/oidcclient"
@@ -42,7 +43,7 @@ func TestCLIGetKubeconfigStaticToken_Parallel(t *testing.T) {
 	ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute)
 	defer cancelFunc()
 
-	authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t)
+	authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady)
 
 	// Build pinniped CLI.
 	pinnipedExe := testlib.PinnipedCLIPath(t)
diff --git a/test/integration/concierge_api_serving_certs_test.go b/test/integration/concierge_api_serving_certs_test.go
index 1162d03c8..839a31719 100644
--- a/test/integration/concierge_api_serving_certs_test.go
+++ b/test/integration/concierge_api_serving_certs_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package integration
@@ -12,6 +12,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
 
+	"go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 	loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
 	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/test/testlib"
@@ -83,7 +84,7 @@ func TestAPIServingCertificateAutoCreationAndRotation_Disruptive(t *testing.T) {
 
 			// Create a testWebhook so we have a legitimate authenticator to pass to the
 			// TokenCredentialRequest API.
-			testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t)
+			testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady)
 
 			// Get the initial auto-generated version of the Secret.
 			secret, err := kubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, defaultServingCertResourceName, metav1.GetOptions{})
diff --git a/test/integration/concierge_client_test.go b/test/integration/concierge_client_test.go
index 8a36f7f44..824d50068 100644
--- a/test/integration/concierge_client_test.go
+++ b/test/integration/concierge_client_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package integration
@@ -11,6 +11,7 @@ import (
 
 	"github.com/stretchr/testify/require"
 
+	"go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 	"go.pinniped.dev/internal/here"
 	"go.pinniped.dev/pkg/conciergeclient"
 	"go.pinniped.dev/test/testlib"
@@ -58,7 +59,7 @@ func TestClient(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
 
-	webhook := testlib.CreateTestWebhookAuthenticator(ctx, t)
+	webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady)
 
 	// Use an invalid certificate/key to validate that the ServerVersion API fails like we assume.
 	invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey)
diff --git a/test/integration/concierge_credentialrequest_test.go b/test/integration/concierge_credentialrequest_test.go
index f05c93134..c7625dcba 100644
--- a/test/integration/concierge_credentialrequest_test.go
+++ b/test/integration/concierge_credentialrequest_test.go
@@ -60,8 +60,15 @@ func TestSuccessfulCredentialRequest_Browser(t *testing.T) {
 		token         func(t *testing.T) (token string, username string, groups []string)
 	}{
 		{
-			name:          "webhook",
-			authenticator: testlib.CreateTestWebhookAuthenticator,
+			name: "webhook",
+			authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
+				authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady)
+				return corev1.TypedLocalObjectReference{
+					APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
+					Kind:     "WebhookAuthenticator",
+					Name:     authenticator.Name,
+				}
+			},
 			token: func(t *testing.T) (string, string, []string) {
 				return testlib.IntegrationEnv(t).TestUser.Token, env.TestUser.ExpectedUsername, env.TestUser.ExpectedGroups
 			},
@@ -148,7 +155,7 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic
 	// TokenCredentialRequest API.
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
-	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t)
+	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady)
 
 	response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
 		loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token", Authenticator: testWebhook},
@@ -169,7 +176,7 @@ func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken_Parallel(t *
 	// TokenCredentialRequest API.
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
-	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t)
+	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady)
 
 	response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
 		loginv1alpha1.TokenCredentialRequestSpec{Token: "", Authenticator: testWebhook},
diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go
index 8df6ad238..d8263881a 100644
--- a/test/integration/concierge_impersonation_proxy_test.go
+++ b/test/integration/concierge_impersonation_proxy_test.go
@@ -61,6 +61,7 @@ import (
 	"k8s.io/client-go/util/retry"
 	"k8s.io/utils/ptr"
 
+	"go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 	conciergev1alpha "go.pinniped.dev/generated/latest/apis/concierge/config/v1alpha1"
 	identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
 	loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
@@ -120,7 +121,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
 	// Create a WebhookAuthenticator and prepare a TokenCredentialRequestSpec using the authenticator for use later.
 	credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{
 		Token:         env.TestUser.Token,
-		Authenticator: testlib.CreateTestWebhookAuthenticator(ctx, t),
+		Authenticator: testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady),
 	}
 
 	// The address of the ClusterIP service that points at the impersonation proxy's port (used when there is no load balancer).
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index 558217660..29935a0e7 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -3,8 +3,294 @@
 
 package integration
 
-// TODO: for integration tests, not unit tests....
-// env.APIGroupSuffix for log messages/conditions when relevant, ie if "pinniped.dev" appears
-// env.CLIUpstreamOIDC.Issuer if a real endpoint is needed as we shouldn't actually make requests
-//   to example.com
-// goodEndpoint := "https://example.com"
+import (
+	"context"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	"go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
+	"go.pinniped.dev/test/testlib"
+)
+
+func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
+	testEnv := testlib.IntegrationEnv(t)
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+	t.Cleanup(cancel)
+
+	tests := []struct {
+		name string
+		run  func(t *testing.T)
+	}{
+		{
+			name: "Basic test to see if the WebhookAuthenticator wakes up or not.",
+			run: func(t *testing.T) {
+				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
+					ctx,
+					t,
+					nil,
+					v1alpha1.WebhookAuthenticatorPhaseReady)
+
+				testlib.WaitForWebhookAuthenticatorStatusConditions(
+					ctx, t,
+					webhookAuthenticator.Name,
+					allSuccessfulWebhookAuthenticatorConditions())
+			},
+		}, {
+			name: "valid spec with invalid CA in TLS config will result in a WebhookAuthenticator that is not ready",
+			run: func(t *testing.T) {
+				caBundleString := "invalid base64-encoded data"
+				webhookSpec := testEnv.TestWebhook.DeepCopy()
+				webhookSpec.TLS = &v1alpha1.TLSSpec{
+					CertificateAuthorityData: caBundleString,
+				}
+
+				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
+					ctx,
+					t,
+					webhookSpec,
+					v1alpha1.WebhookAuthenticatorPhaseError)
+
+				testlib.WaitForWebhookAuthenticatorStatusConditions(
+					ctx, t,
+					webhookAuthenticator.Name,
+					replaceSomeConditions(
+						allSuccessfulWebhookAuthenticatorConditions(),
+						[]metav1.Condition{
+							{
+								Type:    "Ready",
+								Status:  "False",
+								Reason:  "NotReady",
+								Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+							}, {
+								Type:    "AuthenticatorValid",
+								Status:  "Unknown",
+								Reason:  "UnableToValidate",
+								Message: "unable to validate; see other conditions for details",
+							}, {
+								Type:    "TLSConfigurationValid",
+								Status:  "False",
+								Reason:  "InvalidTLSConfiguration",
+								Message: "invalid TLS configuration: illegal base64 data at input byte 7",
+							}, {
+								Type:    "TLSConnectionNegotiationValid",
+								Status:  "Unknown",
+								Reason:  "UnableToValidate",
+								Message: "unable to validate; see other conditions for details",
+							},
+						},
+					))
+			},
+		}, {
+			name: "valid spec with valid CA in TLS config but does not match issuer server will result in a WebhookAuthenticator that is not ready",
+			run: func(t *testing.T) {
+				caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
+				webhookSpec := testEnv.TestWebhook.DeepCopy()
+				webhookSpec.TLS = &v1alpha1.TLSSpec{
+					CertificateAuthorityData: caBundleSomePivotalCA,
+				}
+
+				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
+					ctx,
+					t,
+					webhookSpec,
+					v1alpha1.WebhookAuthenticatorPhaseError)
+
+				testlib.WaitForWebhookAuthenticatorStatusConditions(
+					ctx, t,
+					webhookAuthenticator.Name,
+					replaceSomeConditions(
+						allSuccessfulWebhookAuthenticatorConditions(),
+						[]metav1.Condition{
+							{
+								Type:    "Ready",
+								Status:  "False",
+								Reason:  "NotReady",
+								Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+							}, {
+								Type:    "AuthenticatorValid",
+								Status:  "Unknown",
+								Reason:  "UnableToValidate",
+								Message: "unable to validate; see other conditions for details",
+							}, {
+								Type:    "TLSConnectionNegotiationValid",
+								Status:  "False",
+								Reason:  "UnableToDialServer",
+								Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
+							},
+						},
+					))
+			},
+		}, {
+			name: "invalid with unresponsive endpoint will result in a WebhookAuthenticator that is not ready",
+			run: func(t *testing.T) {
+				caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
+				webhookSpec := testEnv.TestWebhook.DeepCopy()
+				webhookSpec.TLS = &v1alpha1.TLSSpec{
+					CertificateAuthorityData: caBundleSomePivotalCA,
+				}
+				webhookSpec.Endpoint = "https://127.0.0.1:443/some-fake-endpoint"
+
+				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
+					ctx,
+					t,
+					webhookSpec,
+					v1alpha1.WebhookAuthenticatorPhaseError)
+
+				testlib.WaitForWebhookAuthenticatorStatusConditions(
+					ctx, t,
+					webhookAuthenticator.Name,
+					replaceSomeConditions(
+						allSuccessfulWebhookAuthenticatorConditions(),
+						[]metav1.Condition{
+							{
+								Type:    "Ready",
+								Status:  "False",
+								Reason:  "NotReady",
+								Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+							}, {
+								Type:    "AuthenticatorValid",
+								Status:  "Unknown",
+								Reason:  "UnableToValidate",
+								Message: "unable to validate; see other conditions for details",
+							}, {
+								Type:    "TLSConnectionNegotiationValid",
+								Status:  "False",
+								Reason:  "UnableToDialServer",
+								Message: "cannot dial server: dial tcp 127.0.0.1:443: connect: connection refused",
+							},
+						},
+					))
+			},
+		},
+	}
+	for _, test := range tests {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			tt.run(t)
+		})
+	}
+}
+
+func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) {
+	env := testlib.IntegrationEnv(t)
+	webhookAuthenticatorClient := testlib.NewConciergeClientset(t).AuthenticationV1alpha1().WebhookAuthenticators()
+
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+	t.Cleanup(cancel)
+
+	objectMeta := testlib.ObjectMetaWithRandomName(t, "webhook-authenticator")
+	tests := []struct {
+		name                 string
+		webhookAuthenticator *v1alpha1.WebhookAuthenticator
+		wantErr              string
+	}{
+		{
+			name: "endpoint can not be empty string",
+			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
+				ObjectMeta: objectMeta,
+				Spec: v1alpha1.WebhookAuthenticatorSpec{
+					Endpoint: "",
+				},
+			},
+			wantErr: `WebhookAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` +
+				`spec.endpoint: Invalid value: "": spec.endpoint in body should be at least 1 chars long`,
+		},
+		{
+			name: "endpoint must be https",
+			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
+				ObjectMeta: objectMeta,
+				Spec: v1alpha1.WebhookAuthenticatorSpec{
+					Endpoint: "http://www.example.com",
+				},
+			},
+			wantErr: `WebhookAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` +
+				`spec.endpoint: Invalid value: "http://www.example.com": spec.endpoint in body should match '^https://'`,
+		},
+		{
+			name: "minimum valid authenticator",
+			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
+				ObjectMeta: testlib.ObjectMetaWithRandomName(t, "webhook"),
+				Spec: v1alpha1.WebhookAuthenticatorSpec{
+					Endpoint: "https://localhost/webhook-isnt-actually-here",
+				},
+			},
+		},
+		{
+			name: "valid authenticator can have empty TLS block",
+			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
+				ObjectMeta: testlib.ObjectMetaWithRandomName(t, "webhook"),
+				Spec: v1alpha1.WebhookAuthenticatorSpec{
+					Endpoint: "https://localhost/webhook-isnt-actually-here",
+					TLS:      &v1alpha1.TLSSpec{},
+				},
+			},
+		},
+		{
+			name: "valid authenticator can have empty TLS CertificateAuthorityData",
+			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
+				ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
+				Spec: v1alpha1.WebhookAuthenticatorSpec{
+					Endpoint: "https://localhost/webhook-isnt-actually-here",
+					TLS: &v1alpha1.TLSSpec{
+						CertificateAuthorityData: "pretend-this-is-a-certificate",
+					},
+				},
+			},
+		},
+	}
+	for _, test := range tests {
+		tt := test
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			_, createErr := webhookAuthenticatorClient.Create(ctx, tt.webhookAuthenticator, metav1.CreateOptions{})
+
+			t.Cleanup(func() {
+				// delete if it exists
+				delErr := webhookAuthenticatorClient.Delete(ctx, tt.webhookAuthenticator.Name, metav1.DeleteOptions{})
+				if !errors.IsNotFound(delErr) {
+					require.NoError(t, delErr)
+				}
+			})
+
+			if tt.wantErr != "" {
+				wantErr := tt.wantErr
+				require.EqualError(t, createErr, wantErr)
+			} else {
+				require.NoError(t, createErr)
+			}
+		})
+	}
+}
+func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition {
+	return []metav1.Condition{{
+		Type:    "AuthenticatorValid",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "authenticator initialized",
+	}, {
+		Type:    "EndpointURLValid",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "endpoint is a valid URL",
+	}, {
+		Type:    "Ready",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "the WebhookAuthenticator is ready",
+	}, {
+		Type:    "TLSConfigurationValid",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "successfully parsed specified CA bundle",
+	}, {
+		Type:    "TLSConnectionNegotiationValid",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "tls verified",
+	}}
+}
diff --git a/test/integration/kube_api_discovery_test.go b/test/integration/kube_api_discovery_test.go
index f7756f393..327c31a45 100644
--- a/test/integration/kube_api_discovery_test.go
+++ b/test/integration/kube_api_discovery_test.go
@@ -438,7 +438,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
 		}
 
 		// manually update this value whenever you add additional fields to an API resource and then run the generator
-		totalExpectedAPIFields := 260
+		totalExpectedAPIFields := 261
 
 		// Because we are parsing text from `kubectl explain` and because the format of that text can change
 		// over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all
diff --git a/test/testlib/client.go b/test/testlib/client.go
index 4af9c52c9..486e3a92d 100644
--- a/test/testlib/client.go
+++ b/test/testlib/client.go
@@ -36,7 +36,6 @@ import (
 	supervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
 	"go.pinniped.dev/internal/groupsuffix"
 	"go.pinniped.dev/internal/kubeclient"
-
 	// Import to initialize client auth plugins - the kubeconfig that we use for
 	// testing may use gcloud, az, oidc, etc.
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -171,7 +170,11 @@ func NewKubeclient(t *testing.T, config *rest.Config) *kubeclient.Client {
 // CreateTestWebhookAuthenticator creates and returns a test WebhookAuthenticator in $PINNIPED_TEST_CONCIERGE_NAMESPACE, which will be
 // automatically deleted at the end of the current test's lifetime. It returns a corev1.TypedLocalObjectReference which
 // describes the test webhook authenticator within the test namespace.
-func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
+func CreateTestWebhookAuthenticator(
+	ctx context.Context,
+	t *testing.T,
+	webhookSpec *auth1alpha1.WebhookAuthenticatorSpec,
+	expectedStatus auth1alpha1.WebhookAuthenticatorPhase) corev1.TypedLocalObjectReference {
 	t.Helper()
 	testEnv := IntegrationEnv(t)
 
@@ -181,9 +184,13 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty
 	createContext, cancel := context.WithTimeout(ctx, time.Minute)
 	defer cancel()
 
+	if webhookSpec == nil {
+		webhookSpec = &testEnv.TestWebhook
+	}
+
 	webhook, err := webhooks.Create(createContext, &auth1alpha1.WebhookAuthenticator{
 		ObjectMeta: testObjectMeta(t, "webhook"),
-		Spec:       testEnv.TestWebhook,
+		Spec:       *webhookSpec,
 	}, metav1.CreateOptions{})
 	require.NoError(t, err, "could not create test WebhookAuthenticator")
 	t.Logf("created test WebhookAuthenticator %s", webhook.Name)
@@ -197,6 +204,10 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty
 		require.NoErrorf(t, err, "could not cleanup test WebhookAuthenticator %s/%s", webhook.Namespace, webhook.Name)
 	})
 
+	if expectedStatus != "" {
+		WaitForWebhookAuthenticatorStatusPhase(ctx, t, webhook.Name, expectedStatus)
+	}
+
 	return corev1.TypedLocalObjectReference{
 		APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
 		Kind:     "WebhookAuthenticator",
@@ -204,6 +215,47 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty
 	}
 }
 
+func WaitForWebhookAuthenticatorStatusPhase(
+	ctx context.Context,
+	t *testing.T,
+	webhookName string,
+	expectPhase auth1alpha1.WebhookAuthenticatorPhase) {
+	t.Helper()
+	webhookAuthenticatorClientSet := NewConciergeClientset(t).AuthenticationV1alpha1().WebhookAuthenticators()
+
+	RequireEventuallyf(t, func(requireEventually *require.Assertions) {
+		webhookA, err := webhookAuthenticatorClientSet.Get(ctx, webhookName, metav1.GetOptions{})
+		requireEventually.NoError(err)
+		requireEventually.Equalf(expectPhase, webhookA.Status.Phase, "actual status conditions were: %#v", webhookA.Status.Conditions)
+	}, 60*time.Second, 1*time.Second, "expected the WebhookAuthenticator to have status %q", expectPhase)
+}
+
+func WaitForWebhookAuthenticatorStatusConditions(ctx context.Context, t *testing.T, webhookName string, expectConditions []metav1.Condition) {
+	t.Helper()
+	webhookClient := NewConciergeClientset(t).AuthenticationV1alpha1().WebhookAuthenticators()
+	RequireEventuallyf(t, func(requireEventually *require.Assertions) {
+		fd, err := webhookClient.Get(ctx, webhookName, metav1.GetOptions{})
+		requireEventually.NoError(err)
+
+		requireEventually.Lenf(fd.Status.Conditions, len(expectConditions),
+			"wanted status conditions: %#v", expectConditions)
+
+		for i, wantCond := range expectConditions {
+			actualCond := fd.Status.Conditions[i]
+
+			// This is a cheat to avoid needing to make equality assertions on these fields.
+			requireEventually.NotZero(actualCond.LastTransitionTime)
+			wantCond.LastTransitionTime = actualCond.LastTransitionTime
+			requireEventually.NotZero(actualCond.ObservedGeneration)
+			wantCond.ObservedGeneration = actualCond.ObservedGeneration
+
+			requireEventually.Equalf(wantCond, actualCond,
+				"wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d",
+				expectConditions, fd.Status.Conditions, i)
+		}
+	}, 60*time.Second, 1*time.Second, "wanted WebhookAuthenticator conditions")
+}
+
 // CreateTestJWTAuthenticatorForCLIUpstream creates and returns a test JWTAuthenticator which will be automatically
 // deleted at the end of the current test's lifetime.
 //

From 5c1fa6d52c8931649b0638dc244c1646bc87b610 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Wed, 13 Mar 2024 12:30:35 -0400
Subject: [PATCH 04/17] Adjust testlib/client.go for lint quirk

---
 test/testlib/client.go | 1 +
 1 file changed, 1 insertion(+)

diff --git a/test/testlib/client.go b/test/testlib/client.go
index 486e3a92d..ed06346bf 100644
--- a/test/testlib/client.go
+++ b/test/testlib/client.go
@@ -36,6 +36,7 @@ import (
 	supervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
 	"go.pinniped.dev/internal/groupsuffix"
 	"go.pinniped.dev/internal/kubeclient"
+
 	// Import to initialize client auth plugins - the kubeconfig that we use for
 	// testing may use gcloud, az, oidc, etc.
 	_ "k8s.io/client-go/plugin/pkg/client/auth"

From 0467e5c1d552e49eefb82cb50eb94cff41b9ac35 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Mon, 18 Mar 2024 12:05:21 -0400
Subject: [PATCH 05/17] Refactor logLines to SplitByNewline, deduplicate

---
 cmd/pinniped/cmd/login_oidc_test.go           | 12 ++---------
 cmd/pinniped/cmd/login_static_test.go         |  5 +++--
 .../jwtcachefiller/jwtcachefiller_test.go     | 20 +++++++++----------
 .../webhookcachefiller_test.go                |  8 ++++----
 .../kubecertagent/kubecertagent_test.go       | 12 ++---------
 .../testutil/conciergetestutil/tlstestutil.go |  2 +-
 .../loglines.go => stringutil/stringutil.go}  |  4 ++--
 7 files changed, 24 insertions(+), 39 deletions(-)
 rename internal/testutil/{testlogger/loglines.go => stringutil/stringutil.go} (77%)

diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go
index f023e0757..6d28d7b6e 100644
--- a/cmd/pinniped/cmd/login_oidc_test.go
+++ b/cmd/pinniped/cmd/login_oidc_test.go
@@ -10,7 +10,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"strings"
 	"testing"
 	"time"
 
@@ -22,6 +21,7 @@ import (
 	"go.pinniped.dev/internal/certauthority"
 	"go.pinniped.dev/internal/here"
 	"go.pinniped.dev/internal/plog"
+	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/pkg/conciergeclient"
 	"go.pinniped.dev/pkg/oidcclient"
 	"go.pinniped.dev/pkg/oidcclient/oidctypes"
@@ -596,15 +596,7 @@ func TestLoginOIDCCommand(t *testing.T) {
 			require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
 			require.Len(t, gotOptions, tt.wantOptionsCount)
 
-			require.Equal(t, tt.wantLogs, logLines(buf.String()))
+			require.Equal(t, tt.wantLogs, stringutil.SplitByNewline(buf.String()))
 		})
 	}
 }
-
-func logLines(logs string) []string {
-	if len(logs) == 0 {
-		return nil
-	}
-
-	return strings.Split(strings.TrimSpace(logs), "\n")
-}
diff --git a/cmd/pinniped/cmd/login_static_test.go b/cmd/pinniped/cmd/login_static_test.go
index e2f520bdc..bf65a4e39 100644
--- a/cmd/pinniped/cmd/login_static_test.go
+++ b/cmd/pinniped/cmd/login_static_test.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package cmd
@@ -20,6 +20,7 @@ import (
 	"go.pinniped.dev/internal/certauthority"
 	"go.pinniped.dev/internal/here"
 	"go.pinniped.dev/internal/plog"
+	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/pkg/conciergeclient"
 )
 
@@ -215,7 +216,7 @@ func TestLoginStaticCommand(t *testing.T) {
 			require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
 			require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
 
-			require.Equal(t, tt.wantLogs, logLines(buf.String()))
+			require.Equal(t, tt.wantLogs, stringutil.SplitByNewline(buf.String()))
 		})
 	}
 }
diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index 0efc42354..cb9f3d851 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -45,7 +45,7 @@ import (
 	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/internal/testutil/conciergetestutil"
 	"go.pinniped.dev/internal/testutil/conditionstestutil"
-	"go.pinniped.dev/internal/testutil/testlogger"
+	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/internal/testutil/tlsserver"
 )
 
@@ -208,12 +208,12 @@ func TestController(t *testing.T) {
 	someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   goodIssuer,
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 	}
 	someJWTAuthenticatorSpecWithUsernameClaim := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   goodIssuer,
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 		Claims: auth1alpha1.JWTTokenClaims{
 			Username: "my-custom-username-claim",
 		},
@@ -221,7 +221,7 @@ func TestController(t *testing.T) {
 	someJWTAuthenticatorSpecWithGroupsClaim := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   goodIssuer,
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 		Claims: auth1alpha1.JWTTokenClaims{
 			Groups: customGroupsClaim,
 		},
@@ -247,12 +247,12 @@ func TestController(t *testing.T) {
 	invalidIssuerJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   "https://.café   .com/café/café/café/coffee",
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 	}
 	invalidIssuerSchemeJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   "http://.café.com/café/café/café/coffee",
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 	}
 
 	validIssuerURLButDoesNotExistJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
@@ -262,18 +262,18 @@ func TestController(t *testing.T) {
 	badIssuerJWKSURIJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   badIssuerInvalidJWKSURI,
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURI.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURI.TLS),
 	}
 	badIssuerJWKSURISchemeJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   badIssuerInvalidJWKSURIScheme,
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIScheme.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIScheme.TLS),
 	}
 
 	jwksFetchShouldFailJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
 		Issuer:   jwksFetchShouldFailServer.URL,
 		Audience: goodAudience,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(jwksFetchShouldFailServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(jwksFetchShouldFailServer.TLS),
 	}
 
 	happyReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
@@ -1454,7 +1454,7 @@ func TestController(t *testing.T) {
 				require.NoError(t, err)
 			}
 
-			actualLogLines := testlogger.LogLines(log.String())
+			actualLogLines := stringutil.SplitByNewline(log.String())
 			require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct")
 
 			for logLineNum, logLine := range actualLogLines {
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index e21e7b45c..a8ffc4c67 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -42,7 +42,7 @@ import (
 	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/internal/testutil/conciergetestutil"
 	"go.pinniped.dev/internal/testutil/conditionstestutil"
-	"go.pinniped.dev/internal/testutil/testlogger"
+	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/internal/testutil/tlsserver"
 )
 
@@ -162,7 +162,7 @@ func TestController(t *testing.T) {
 
 	goodWebhookAuthenticatorSpecWithCA := auth1alpha1.WebhookAuthenticatorSpec{
 		Endpoint: goodEndpoint,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodWebhookServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodWebhookServer.TLS),
 	}
 	localhostWebhookAuthenticatorSpecWithCA := auth1alpha1.WebhookAuthenticatorSpec{
 		Endpoint: localhostEndpointURL,
@@ -185,7 +185,7 @@ func TestController(t *testing.T) {
 	}
 	goodWebhookAuthenticatorSpecWith404Endpoint := auth1alpha1.WebhookAuthenticatorSpec{
 		Endpoint: goodEndpointBut404,
-		TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodWebhookServer.TLS),
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodWebhookServer.TLS),
 	}
 	badWebhookAuthenticatorSpecInvalidTLS := auth1alpha1.WebhookAuthenticatorSpec{
 		Endpoint: goodEndpoint,
@@ -933,7 +933,7 @@ func TestController(t *testing.T) {
 			} else {
 				require.NoError(t, err)
 			}
-			actualLogLines := testlogger.LogLines(log.String())
+			actualLogLines := stringutil.SplitByNewline(log.String())
 			require.Equal(t, len(actualLogLines), len(tt.wantLogs), "log line count should be correct")
 
 			for logLineNum, logLine := range actualLogLines {
diff --git a/internal/controller/kubecertagent/kubecertagent_test.go b/internal/controller/kubecertagent/kubecertagent_test.go
index f8fb5482d..c8dfd0fa9 100644
--- a/internal/controller/kubecertagent/kubecertagent_test.go
+++ b/internal/controller/kubecertagent/kubecertagent_test.go
@@ -7,7 +7,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"strings"
 	"testing"
 	"time"
 
@@ -39,6 +38,7 @@ import (
 	"go.pinniped.dev/internal/kubeclient"
 	"go.pinniped.dev/internal/plog"
 	"go.pinniped.dev/internal/testutil"
+	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/test/testlib"
 )
 
@@ -1085,7 +1085,7 @@ func TestAgentController(t *testing.T) {
 			allAllowedErrors = append(allAllowedErrors, tt.alsoAllowUndesiredDistinctErrors...)
 			assert.Subsetf(t, allAllowedErrors, actualErrors, "actual errors contained additional error(s) which is not expected by the test")
 
-			assert.Equal(t, tt.wantDistinctLogs, deduplicate(logLines(buf.String())), "unexpected logs")
+			assert.Equal(t, tt.wantDistinctLogs, deduplicate(stringutil.SplitByNewline(buf.String())), "unexpected logs")
 
 			// Assert on all actions that happened to deployments.
 			var actualDeploymentActionVerbs []string
@@ -1128,14 +1128,6 @@ func TestAgentController(t *testing.T) {
 	}
 }
 
-func logLines(logs string) []string {
-	if len(logs) == 0 {
-		return nil
-	}
-
-	return strings.Split(strings.TrimSpace(logs), "\n")
-}
-
 func TestMergeLabelsAndAnnotations(t *testing.T) {
 	t.Parallel()
 
diff --git a/internal/testutil/conciergetestutil/tlstestutil.go b/internal/testutil/conciergetestutil/tlstestutil.go
index 0dc412e61..f99ba4e86 100644
--- a/internal/testutil/conciergetestutil/tlstestutil.go
+++ b/internal/testutil/conciergetestutil/tlstestutil.go
@@ -11,7 +11,7 @@ import (
 	auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
 )
 
-func TlsSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
+func TLSSpecFromTLSConfig(tls *tls.Config) *auth1alpha1.TLSSpec {
 	pemData := make([]byte, 0)
 	for _, certificate := range tls.Certificates {
 		// this is the public part of the certificate, the private is the certificate.PrivateKey
diff --git a/internal/testutil/testlogger/loglines.go b/internal/testutil/stringutil/stringutil.go
similarity index 77%
rename from internal/testutil/testlogger/loglines.go
rename to internal/testutil/stringutil/stringutil.go
index 8ec15f320..aa8a266f1 100644
--- a/internal/testutil/testlogger/loglines.go
+++ b/internal/testutil/stringutil/stringutil.go
@@ -1,11 +1,11 @@
 // Copyright 2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
-package testlogger
+package stringutil
 
 import "strings"
 
-func LogLines(logs string) []string {
+func SplitByNewline(logs string) []string {
 	if len(logs) == 0 {
 		return nil
 	}

From a45a537cdbd43dc5556eb30d85101f4869f065b2 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Wed, 13 Mar 2024 17:18:57 -0400
Subject: [PATCH 06/17] Improve JWTAuthenticator validation of Issuer,Discovery

---
 .../jwtcachefiller/jwtcachefiller.go          |  65 ++++-
 .../jwtcachefiller/jwtcachefiller_test.go     | 249 ++++++++++++++++++
 .../oidc_upstream_watcher.go                  |  20 +-
 internal/controller/utilserr.go               |  21 ++
 4 files changed, 325 insertions(+), 30 deletions(-)
 create mode 100644 internal/controller/utilserr.go

diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
index c4dd46884..951db2bfa 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
@@ -53,18 +53,21 @@ const (
 	typeJWKSFetchValid        = "JWKSFetchValid"
 	typeAuthenticatorValid    = "AuthenticatorValid"
 
-	reasonSuccess                      = "Success"
-	reasonNotReady                     = "NotReady"
-	reasonUnableToValidate             = "UnableToValidate"
-	reasonInvalidIssuerURL             = "InvalidIssuerURL"
-	reasonInvalidIssuerURLScheme       = "InvalidIssuerURLScheme"
-	reasonInvalidProviderJWKSURL       = "InvalidProviderJWKSURL"
-	reasonInvalidProviderJWKSURLScheme = "InvalidProviderJWKSURLScheme"
-	reasonInvalidTLSConfiguration      = "InvalidTLSConfiguration"
-	reasonInvalidDiscoveryProbe        = "InvalidDiscoveryProbe"
-	reasonInvalidAuthenticator         = "InvalidAuthenticator"
-	reasonInvalidTokenSigningFailure   = "InvalidTokenSigningFailure"
-	reasonInvalidCouldNotFetchJWKS     = "InvalidCouldNotFetchJWKS"
+	reasonSuccess                                   = "Success"
+	reasonNotReady                                  = "NotReady"
+	reasonUnableToValidate                          = "UnableToValidate"
+	reasonInvalidIssuerURL                          = "InvalidIssuerURL"
+	reasonInvalidIssuerURLScheme                    = "InvalidIssuerURLScheme"
+	reasonInvalidIssuerURLFragment                  = "InvalidIssuerURLContainsFragment"
+	reasonInvalidIssuerURLQueryParams               = "InvalidIssuerURLContainsQueryParams"
+	reasonInvalidIssuerURLContainsWellKnownEndpoint = "InvalidIssuerURLContainsWellKnownEndpoint"
+	reasonInvalidProviderJWKSURL                    = "InvalidProviderJWKSURL"
+	reasonInvalidProviderJWKSURLScheme              = "InvalidProviderJWKSURLScheme"
+	reasonInvalidTLSConfiguration                   = "InvalidTLSConfiguration"
+	reasonInvalidDiscoveryProbe                     = "InvalidDiscoveryProbe"
+	reasonInvalidAuthenticator                      = "InvalidAuthenticator"
+	reasonInvalidTokenSigningFailure                = "InvalidTokenSigningFailure"
+	reasonInvalidCouldNotFetchJWKS                  = "InvalidCouldNotFetchJWKS"
 
 	msgUnableToValidate = "unable to validate; see other conditions for details"
 
@@ -286,6 +289,39 @@ func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*m
 		return nil, conditions, false
 	}
 
+	if strings.HasSuffix(issuerURL.Path, "/.well-known/openid-configuration") {
+		msg := fmt.Sprintf("spec.issuer %s cannot include path '/.well-known/openid-configuration'", issuer)
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeIssuerURLValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidIssuerURLContainsWellKnownEndpoint,
+			Message: msg,
+		})
+		return nil, conditions, false
+	}
+
+	if len(issuerURL.Query()) != 0 {
+		msg := fmt.Sprintf("spec.issuer %s cannot include query params", issuer)
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeIssuerURLValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidIssuerURLQueryParams,
+			Message: msg,
+		})
+		return nil, conditions, false
+	}
+
+	if issuerURL.Fragment != "" {
+		msg := fmt.Sprintf("spec.issuer %s cannot include fragment", issuer)
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeIssuerURLValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidIssuerURLFragment,
+			Message: msg,
+		})
+		return nil, conditions, false
+	}
+
 	conditions = append(conditions, &metav1.Condition{
 		Type:    typeIssuerURLValid,
 		Status:  metav1.ConditionTrue,
@@ -305,11 +341,12 @@ func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context
 		})
 		return nil, nil, conditions, nil
 	}
+
 	provider, err := coreosoidc.NewProvider(ctx, issuer)
 	pJSON := &providerJSON{}
 	if err != nil {
 		errText := "could not perform oidc discovery on provider issuer"
-		msg := fmt.Sprintf("%s: %s", errText, err.Error())
+		msg := fmt.Sprintf("%s: %s", errText, pinnipedcontroller.TruncateMostLongErr(err))
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeDiscoveryValid,
 			Status:  metav1.ConditionFalse,
@@ -317,7 +354,7 @@ func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context
 			Message: msg,
 		})
 		// resync err, may be machine or other types of non-config error
-		return nil, nil, conditions, fmt.Errorf("%s: %w", errText, err)
+		return nil, nil, conditions, fmt.Errorf("%s: %s", errText, err)
 	}
 	msg := "discovery performed successfully"
 	conditions = append(conditions, &metav1.Condition{
diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index cb9f3d851..4b13a500b 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -87,6 +87,20 @@ func TestController(t *testing.T) {
 		_, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, goodOIDCIssuerServer.URL, goodOIDCIssuerServer.URL+"/jwks.json")
 		require.NoError(t, err)
 	}))
+	goodMux.Handle("/path/to/not/found", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNotFound)
+		require.NoError(t, err)
+	}))
+	goodMux.Handle("/path/to/not/found/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(http.StatusNotFound)
+		_, err := fmt.Fprintf(w, `<html>
+		  	<head><title>%s</title></head>
+			<body>%s</body>
+		</html>`, "404 not found page", "lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz should not reach end of string")
+		require.NoError(t, err)
+	}))
 	goodMux.Handle("/jwks.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		ecJWK := jose.JSONWebKey{
 			Key:       goodECSigningKey,
@@ -360,6 +374,39 @@ func TestController(t *testing.T) {
 		}
 	}
 
+	sadIssuerURLValidInvalidFragment := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "IssuerURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidIssuerURLContainsFragment",
+			Message:            fmt.Sprintf("spec.issuer %s cannot include fragment", issuer),
+		}
+	}
+
+	sadIssuerURLValidInvalidQueryParams := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "IssuerURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidIssuerURLContainsQueryParams",
+			Message:            fmt.Sprintf("spec.issuer %s cannot include query params", issuer),
+		}
+	}
+
+	sadIssuerURLValidInvalidWellKnownEndpoint := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "IssuerURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidIssuerURLContainsWellKnownEndpoint",
+			Message:            fmt.Sprintf("spec.issuer %s cannot include path '/.well-known/openid-configuration'", issuer),
+		}
+	}
+
 	happyAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
 			Type:               "AuthenticatorValid",
@@ -428,6 +475,17 @@ func TestController(t *testing.T) {
 		}
 	}
 
+	sadDiscoveryURLValidExcessiveLongError := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+		return metav1.Condition{
+			Type:               "DiscoveryURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidDiscoveryProbe",
+			Message:            "could not perform oidc discovery on provider issuer: 404 Not Found: <html>\n\t\t  \t<head><title>404 not found page</title></head>\n\t\t\t<body>lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 charact [truncated 534 chars]",
+		}
+	}
+
 	happyJWKSURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
 			Type:               "JWKSURLValid",
@@ -1095,6 +1153,147 @@ func TestController(t *testing.T) {
 					updateStatusAction,
 				}
 			},
+		}, {
+			name: "validateIssuer: issuer cannot include fragment: loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error",
+			jwtAuthenticators: []runtime.Object{
+				&auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   "https://www.example.com/foo/bar/#do-not-include-fragment",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+				},
+			},
+			syncKey: controllerlib.Key{Name: "test-name"},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   "https://www.example.com/foo/bar/#do-not-include-fragment",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+					Status: auth1alpha1.JWTAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadReadyCondition(frozenMetav1Now, 0),
+								sadIssuerURLValidInvalidFragment("https://www.example.com/foo/bar/#do-not-include-fragment", frozenMetav1Now, 0),
+								unknownDiscoveryURLValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								unknownJWKSURLValid(frozenMetav1Now, 0),
+								unknownJWKSFetch(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+		}, {
+			name: "validateIssuer: issuer cannot include query params: loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error",
+			jwtAuthenticators: []runtime.Object{
+				&auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   "https://www.example.com/foo/bar/?query-params=not-allowed",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+				},
+			},
+			syncKey: controllerlib.Key{Name: "test-name"},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   "https://www.example.com/foo/bar/?query-params=not-allowed",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+					Status: auth1alpha1.JWTAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadReadyCondition(frozenMetav1Now, 0),
+								sadIssuerURLValidInvalidQueryParams("https://www.example.com/foo/bar/?query-params=not-allowed", frozenMetav1Now, 0),
+								unknownDiscoveryURLValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								unknownJWKSURLValid(frozenMetav1Now, 0),
+								unknownJWKSFetch(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+		}, {
+			name: "validateIssuer: issuer cannot include .well-known in path: loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error",
+			jwtAuthenticators: []runtime.Object{
+				&auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   "https://www.example.com/foo/bar/.well-known/openid-configuration",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+				},
+			},
+			syncKey: controllerlib.Key{Name: "test-name"},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   "https://www.example.com/foo/bar/.well-known/openid-configuration",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+					Status: auth1alpha1.JWTAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadReadyCondition(frozenMetav1Now, 0),
+								sadIssuerURLValidInvalidWellKnownEndpoint("https://www.example.com/foo/bar/.well-known/openid-configuration", frozenMetav1Now, 0),
+								unknownDiscoveryURLValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								unknownJWKSURLValid(frozenMetav1Now, 0),
+								unknownJWKSFetch(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
 		}, {
 			name: "validateProviderDiscovery: could not perform oidc discovery on provider issuer: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync",
 			jwtAuthenticators: []runtime.Object{
@@ -1136,6 +1335,56 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantSyncLoopErr: testutil.WantExactErrorString(`could not perform oidc discovery on provider issuer: Get "` + goodIssuer + `/foo/bar/baz/shizzle/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`),
+		}, {
+			name: "validateProviderDiscovery: excessively long errors truncated: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync",
+			jwtAuthenticators: []runtime.Object{
+				&auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   goodIssuer + "/path/to/not/found",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+				},
+			},
+			syncKey: controllerlib.Key{Name: "test-name"},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.JWTAuthenticatorSpec{
+						Issuer:   goodIssuer + "/path/to/not/found",
+						Audience: goodAudience,
+						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+					},
+					Status: auth1alpha1.JWTAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								happyIssuerURLValid(frozenMetav1Now, 0),
+								sadReadyCondition(frozenMetav1Now, 0),
+								sadDiscoveryURLValidExcessiveLongError(goodIssuer+"/path/to/not/found", frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+								unknownJWKSURLValid(frozenMetav1Now, 0),
+								unknownJWKSFetch(frozenMetav1Now, 0),
+								happyTLSConfigurationValidCAParsed(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			// not currently truncating the logged err
+			wantSyncLoopErr: testutil.WantExactErrorString("could not perform oidc discovery on provider issuer: 404 Not Found: <html>\n\t\t  \t<head><title>404 not found page</title></head>\n\t\t\t<body>lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz lots of text that is at least 300 characters long 0123456789 abcdefghijklmnopqrstuvwxyz should not reach end of string</body>\n\t\t</html>"),
 		},
 		// cannot be tested currently the way the coreos lib works.
 		// the constructor requires an issuer in the payload and validates the issuer matches the actual issuer,
diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go
index 9edf172f7..396df85dc 100644
--- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go
+++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go
@@ -1,4 +1,4 @@
-// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
+// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 // Package oidcupstreamwatcher implements a controller which watches OIDCIdentityProviders.
@@ -342,7 +342,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
 				Type:    typeOIDCDiscoverySucceeded,
 				Status:  metav1.ConditionFalse,
 				Reason:  reasonUnreachable,
-				Message: fmt.Sprintf("failed to perform OIDC discovery against %q:\n%s", upstream.Spec.Issuer, truncateMostLongErr(err)),
+				Message: fmt.Sprintf("failed to perform OIDC discovery against %q:\n%s", upstream.Spec.Issuer, pinnipedcontroller.TruncateMostLongErr(err)),
 			}
 		}
 
@@ -361,7 +361,7 @@ func (c *oidcWatcherController) validateIssuer(ctx context.Context, upstream *v1
 			Type:    typeOIDCDiscoverySucceeded,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonInvalidResponse,
-			Message: fmt.Sprintf("failed to unmarshal OIDC discovery response from %q:\n%s", upstream.Spec.Issuer, truncateMostLongErr(err)),
+			Message: fmt.Sprintf("failed to unmarshal OIDC discovery response from %q:\n%s", upstream.Spec.Issuer, pinnipedcontroller.TruncateMostLongErr(err)),
 		}
 	}
 	if additionalDiscoveryClaims.RevocationEndpoint != "" {
@@ -473,18 +473,6 @@ func computeScopes(additionalScopes []string) []string {
 	return set.List()
 }
 
-func truncateMostLongErr(err error) string {
-	const max = 300
-	msg := err.Error()
-
-	// always log oidc and x509 errors completely
-	if len(msg) <= max || strings.Contains(msg, "oidc:") || strings.Contains(msg, "x509:") {
-		return msg
-	}
-
-	return msg[:max] + fmt.Sprintf(" [truncated %d chars]", len(msg)-max)
-}
-
 func validateHTTPSURL(maybeHTTPSURL, endpointType, reason string) (*url.URL, *metav1.Condition) {
 	parsedURL, err := url.Parse(maybeHTTPSURL)
 	if err != nil {
@@ -492,7 +480,7 @@ func validateHTTPSURL(maybeHTTPSURL, endpointType, reason string) (*url.URL, *me
 			Type:    typeOIDCDiscoverySucceeded,
 			Status:  metav1.ConditionFalse,
 			Reason:  reason,
-			Message: fmt.Sprintf("failed to parse %s URL: %v", endpointType, truncateMostLongErr(err)),
+			Message: fmt.Sprintf("failed to parse %s URL: %v", endpointType, pinnipedcontroller.TruncateMostLongErr(err)),
 		}
 	}
 	if parsedURL.Scheme != "https" {
diff --git a/internal/controller/utilserr.go b/internal/controller/utilserr.go
new file mode 100644
index 000000000..485b2f647
--- /dev/null
+++ b/internal/controller/utilserr.go
@@ -0,0 +1,21 @@
+// Copyright 2024 the Pinniped contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package controller
+
+import (
+	"fmt"
+	"strings"
+)
+
+func TruncateMostLongErr(err error) string {
+	const max = 300
+	msg := err.Error()
+
+	// always log oidc and x509 errors completely
+	if len(msg) <= max || strings.Contains(msg, "oidc:") || strings.Contains(msg, "x509:") {
+		return msg
+	}
+
+	return msg[:max] + fmt.Sprintf(" [truncated %d chars]", len(msg)-max)
+}

From 097e6d5340903dce644d2b8d4adaa5ad9cb58715 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Mon, 18 Mar 2024 16:14:42 -0400
Subject: [PATCH 07/17] Always pass spec to CreateTestWebhookAuthenticator

---
 .../jwtcachefiller/jwtcachefiller_test.go        | 16 ++++++++--------
 .../webhookcachefiller_test.go                   |  6 +-----
 test/integration/cli_test.go                     |  2 +-
 .../concierge_api_serving_certs_test.go          |  2 +-
 test/integration/concierge_client_test.go        |  2 +-
 .../concierge_credentialrequest_test.go          | 11 +++--------
 .../concierge_impersonation_proxy_test.go        |  2 +-
 ...concierge_webhookauthenticator_status_test.go | 14 +++++++++++++-
 test/testlib/client.go                           |  5 -----
 9 files changed, 29 insertions(+), 31 deletions(-)

diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index 4b13a500b..8759f8437 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -1163,7 +1163,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   "https://www.example.com/foo/bar/#do-not-include-fragment",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 				},
 			},
@@ -1176,7 +1176,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   "https://www.example.com/foo/bar/#do-not-include-fragment",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 					Status: auth1alpha1.JWTAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
@@ -1210,7 +1210,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   "https://www.example.com/foo/bar/?query-params=not-allowed",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 				},
 			},
@@ -1223,7 +1223,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   "https://www.example.com/foo/bar/?query-params=not-allowed",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 					Status: auth1alpha1.JWTAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
@@ -1257,7 +1257,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   "https://www.example.com/foo/bar/.well-known/openid-configuration",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 				},
 			},
@@ -1270,7 +1270,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   "https://www.example.com/foo/bar/.well-known/openid-configuration",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 					Status: auth1alpha1.JWTAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
@@ -1345,7 +1345,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   goodIssuer + "/path/to/not/found",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 				},
 			},
@@ -1358,7 +1358,7 @@ func TestController(t *testing.T) {
 					Spec: auth1alpha1.JWTAuthenticatorSpec{
 						Issuer:   goodIssuer + "/path/to/not/found",
 						Audience: goodAudience,
-						TLS:      conciergetestutil.TlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
+						TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
 					},
 					Status: auth1alpha1.JWTAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index a8ffc4c67..bd1558a93 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -99,7 +99,6 @@ func TestController(t *testing.T) {
 	}), func(thisServer *httptest.Server) {
 		thisTLSConfig := ptls.Default(nil)
 		thisTLSConfig.Certificates = []tls.Certificate{
-			// public and private key pair, but server will only use private for serving
 			*hostAsLocalhostServingCert,
 		}
 		thisServer.TLS = thisTLSConfig
@@ -219,6 +218,7 @@ func TestController(t *testing.T) {
 			Message:            "the WebhookAuthenticator is not ready: see other conditions for details",
 		}
 	}
+
 	happyAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
 			Type:               "AuthenticatorValid",
@@ -1038,8 +1038,6 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 			},
 		}, conditions)
 		require.Nil(t, res)
-		// TODO: should this trigger the sync loop again with an error, or should this have been only
-		// status and log, indicating user must correct?
 		require.EqualError(t, err, "invalid TLS configuration: illegal base64 data at input byte 7")
 	})
 
@@ -1079,8 +1077,6 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 	})
 
 	t.Run("success, webhook authenticator created", func(t *testing.T) {
-		// TODO(BEN): when enhancing webhook authenticator integration test, can prob
-		// steal this and create a super simpler server
 		caBundle, url := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
 			body, err := io.ReadAll(r.Body)
 			require.NoError(t, err)
diff --git a/test/integration/cli_test.go b/test/integration/cli_test.go
index 394424da2..0da362f66 100644
--- a/test/integration/cli_test.go
+++ b/test/integration/cli_test.go
@@ -43,7 +43,7 @@ func TestCLIGetKubeconfigStaticToken_Parallel(t *testing.T) {
 	ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute)
 	defer cancelFunc()
 
-	authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady)
+	authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, v1alpha1.WebhookAuthenticatorPhaseReady)
 
 	// Build pinniped CLI.
 	pinnipedExe := testlib.PinnipedCLIPath(t)
diff --git a/test/integration/concierge_api_serving_certs_test.go b/test/integration/concierge_api_serving_certs_test.go
index 839a31719..d5350c056 100644
--- a/test/integration/concierge_api_serving_certs_test.go
+++ b/test/integration/concierge_api_serving_certs_test.go
@@ -84,7 +84,7 @@ func TestAPIServingCertificateAutoCreationAndRotation_Disruptive(t *testing.T) {
 
 			// Create a testWebhook so we have a legitimate authenticator to pass to the
 			// TokenCredentialRequest API.
-			testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady)
+			testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, v1alpha1.WebhookAuthenticatorPhaseReady)
 
 			// Get the initial auto-generated version of the Secret.
 			secret, err := kubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, defaultServingCertResourceName, metav1.GetOptions{})
diff --git a/test/integration/concierge_client_test.go b/test/integration/concierge_client_test.go
index 824d50068..6a7ffae76 100644
--- a/test/integration/concierge_client_test.go
+++ b/test/integration/concierge_client_test.go
@@ -59,7 +59,7 @@ func TestClient(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
 
-	webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady)
+	webhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, v1alpha1.WebhookAuthenticatorPhaseReady)
 
 	// Use an invalid certificate/key to validate that the ServerVersion API fails like we assume.
 	invalidClient := testlib.NewClientsetWithCertAndKey(t, testCert, testKey)
diff --git a/test/integration/concierge_credentialrequest_test.go b/test/integration/concierge_credentialrequest_test.go
index c7625dcba..cadf16de9 100644
--- a/test/integration/concierge_credentialrequest_test.go
+++ b/test/integration/concierge_credentialrequest_test.go
@@ -62,12 +62,7 @@ func TestSuccessfulCredentialRequest_Browser(t *testing.T) {
 		{
 			name: "webhook",
 			authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
-				authenticator := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady)
-				return corev1.TypedLocalObjectReference{
-					APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
-					Kind:     "WebhookAuthenticator",
-					Name:     authenticator.Name,
-				}
+				return testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, auth1alpha1.WebhookAuthenticatorPhaseReady)
 			},
 			token: func(t *testing.T) (string, string, []string) {
 				return testlib.IntegrationEnv(t).TestUser.Token, env.TestUser.ExpectedUsername, env.TestUser.ExpectedGroups
@@ -155,7 +150,7 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic
 	// TokenCredentialRequest API.
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
-	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady)
+	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, auth1alpha1.WebhookAuthenticatorPhaseReady)
 
 	response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
 		loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token", Authenticator: testWebhook},
@@ -176,7 +171,7 @@ func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken_Parallel(t *
 	// TokenCredentialRequest API.
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
-	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, nil, auth1alpha1.WebhookAuthenticatorPhaseReady)
+	testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, auth1alpha1.WebhookAuthenticatorPhaseReady)
 
 	response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
 		loginv1alpha1.TokenCredentialRequestSpec{Token: "", Authenticator: testWebhook},
diff --git a/test/integration/concierge_impersonation_proxy_test.go b/test/integration/concierge_impersonation_proxy_test.go
index d8263881a..38ebb23ce 100644
--- a/test/integration/concierge_impersonation_proxy_test.go
+++ b/test/integration/concierge_impersonation_proxy_test.go
@@ -121,7 +121,7 @@ func TestImpersonationProxy(t *testing.T) { //nolint:gocyclo // yeah, it's compl
 	// Create a WebhookAuthenticator and prepare a TokenCredentialRequestSpec using the authenticator for use later.
 	credentialRequestSpecWithWorkingCredentials := loginv1alpha1.TokenCredentialRequestSpec{
 		Token:         env.TestUser.Token,
-		Authenticator: testlib.CreateTestWebhookAuthenticator(ctx, t, nil, v1alpha1.WebhookAuthenticatorPhaseReady),
+		Authenticator: testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, v1alpha1.WebhookAuthenticatorPhaseReady),
 	}
 
 	// The address of the ClusterIP service that points at the impersonation proxy's port (used when there is no load balancer).
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index 29935a0e7..70a139e98 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -31,7 +31,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
 					ctx,
 					t,
-					nil,
+					&testlib.IntegrationEnv(t).TestWebhook,
 					v1alpha1.WebhookAuthenticatorPhaseReady)
 
 				testlib.WaitForWebhookAuthenticatorStatusConditions(
@@ -232,6 +232,18 @@ func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) {
 		},
 		{
 			name: "valid authenticator can have empty TLS CertificateAuthorityData",
+			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
+				ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
+				Spec: v1alpha1.WebhookAuthenticatorSpec{
+					Endpoint: "https://localhost/webhook-isnt-actually-here",
+					TLS: &v1alpha1.TLSSpec{
+						CertificateAuthorityData: "",
+					},
+				},
+			},
+		}, {
+			// since the CRD validations do not assess fitness of the value provided
+			name: "valid authenticator can have TLS CertificateAuthorityData string that is an invalid certificate",
 			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
 				ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
 				Spec: v1alpha1.WebhookAuthenticatorSpec{
diff --git a/test/testlib/client.go b/test/testlib/client.go
index ed06346bf..9be61147a 100644
--- a/test/testlib/client.go
+++ b/test/testlib/client.go
@@ -177,7 +177,6 @@ func CreateTestWebhookAuthenticator(
 	webhookSpec *auth1alpha1.WebhookAuthenticatorSpec,
 	expectedStatus auth1alpha1.WebhookAuthenticatorPhase) corev1.TypedLocalObjectReference {
 	t.Helper()
-	testEnv := IntegrationEnv(t)
 
 	client := NewConciergeClientset(t)
 	webhooks := client.AuthenticationV1alpha1().WebhookAuthenticators()
@@ -185,10 +184,6 @@ func CreateTestWebhookAuthenticator(
 	createContext, cancel := context.WithTimeout(ctx, time.Minute)
 	defer cancel()
 
-	if webhookSpec == nil {
-		webhookSpec = &testEnv.TestWebhook
-	}
-
 	webhook, err := webhooks.Create(createContext, &auth1alpha1.WebhookAuthenticator{
 		ObjectMeta: testObjectMeta(t, "webhook"),
 		Spec:       *webhookSpec,

From b6512bcbb60d93cc2ec63c454019e1425a213193 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Mon, 18 Mar 2024 17:03:34 -0400
Subject: [PATCH 08/17] add WebhookCacheFiller updateStatus tests

---
 .../jwtcachefiller/jwtcachefiller.go          |   3 +
 .../jwtcachefiller/jwtcachefiller_test.go     |   6 +
 .../webhookcachefiller_test.go                | 162 +++++++++++++++++-
 3 files changed, 168 insertions(+), 3 deletions(-)

diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
index 951db2bfa..f543b2e6f 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
@@ -606,5 +606,8 @@ func (c *jwtCacheFillerController) updateStatus(
 		return nil
 	}
 	_, err := c.client.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
+	if err != nil {
+		c.log.Info(fmt.Sprintf("ERROR: %v", err))
+	}
 	return err
 }
diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index 8759f8437..1ceaa3fa1 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -1652,6 +1652,12 @@ func TestController(t *testing.T) {
 				"jwtAuthenticator": map[string]interface{}{
 					"name": "test-name",
 				},
+			}, {
+				"level":     "info",
+				"timestamp": "2099-08-08T13:57:36.123456Z",
+				"logger":    "jwtcachefiller-controller",
+				"message":   "ERROR: some update error",
+				"issuer":    goodIssuer,
 			}},
 			wantSyncLoopErr:  testutil.WantExactErrorString("some update error"),
 			wantCacheEntries: 1,
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index bd1558a93..4cef02969 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -365,9 +365,11 @@ func TestController(t *testing.T) {
 	}
 
 	tests := []struct {
-		name             string
-		syncKey          controllerlib.Key
-		webhooks         []runtime.Object
+		name     string
+		syncKey  controllerlib.Key
+		webhooks []runtime.Object
+		// for modifying the clients to hack in arbitrary api responses
+		configClient     func(*pinnipedfake.Clientset)
 		tlsDialerFunc    func(network string, addr string, config *tls.Config) (*tls.Conn, error)
 		wantSyncLoopErr  testutil.RequireErrorStringFunc
 		wantLogs         []map[string]any
@@ -895,6 +897,157 @@ func TestController(t *testing.T) {
 			},
 			wantCacheEntries: 0,
 			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`),
+		}, {
+			name:    "updateStatus: called with matching original and updated conditions: will not make request to update conditions",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodWebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "updateStatus: called with different original and updated conditions: will make request to update conditions",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadReadyCondition(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "SomethingBeforeUpdating",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodWebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 1,
+		}, {
+			name:    "updateStatus: when update request fails: error will enqueue a resync",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			configClient: func(client *pinnipedfake.Clientset) {
+				client.PrependReactor(
+					"update",
+					"webhookauthenticators",
+					func(action coretesting.Action) (handled bool, ret runtime.Object, err error) {
+						return true, nil, errors.New("some update error")
+					},
+				)
+			},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadReadyCondition(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "SomethingBeforeUpdating",
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  goodWebhookServer.URL,
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				}, {
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "ERROR: some update error",
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: goodWebhookAuthenticatorSpecWithCA,
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantSyncLoopErr:  testutil.WantExactErrorString("some update error"),
+			wantCacheEntries: 1,
 		},
 	}
 	for _, tt := range tests {
@@ -903,6 +1056,9 @@ func TestController(t *testing.T) {
 			t.Parallel()
 
 			pinnipedAPIClient := pinnipedfake.NewSimpleClientset(tt.webhooks...)
+			if tt.configClient != nil {
+				tt.configClient(pinnipedAPIClient)
+			}
 			informers := pinnipedinformers.NewSharedInformerFactory(pinnipedAPIClient, 0)
 			cache := authncache.New()
 

From 5c0d67dc507da138cfd37e7f172cbe142dce5baf Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Tue, 19 Mar 2024 12:41:36 -0400
Subject: [PATCH 09/17] refactor WebhookAuthenticator newWebhookAuthenticator
 func

---
 .../webhookcachefiller/webhookcachefiller.go  | 48 +++++---------
 .../webhookcachefiller_test.go                | 64 +++++--------------
 2 files changed, 31 insertions(+), 81 deletions(-)

diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index 592e04e9b..f5f8a3123 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -17,7 +17,7 @@ import (
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	errorsutil "k8s.io/apimachinery/pkg/util/errors"
-	"k8s.io/apimachinery/pkg/util/net"
+	k8snetutil "k8s.io/apimachinery/pkg/util/net"
 	"k8s.io/apiserver/pkg/authentication/authenticator"
 	webhookutil "k8s.io/apiserver/pkg/util/webhook"
 	"k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
@@ -112,7 +112,7 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 	specCopy := obj.Spec.DeepCopy()
 	var errs []error
 
-	certPool, conditions, tlsBundleOk := c.validateTLSBundle(specCopy.TLS, conditions)
+	certPool, pemBytes, conditions, tlsBundleOk := c.validateTLSBundle(specCopy.TLS, conditions)
 	endpointURL, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
 	okSoFar := tlsBundleOk && endpointOk
 	conditions, tlsNegotiateErr := c.validateTLSNegotiation(certPool, endpointURL, conditions, okSoFar)
@@ -120,7 +120,8 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 	okSoFar = okSoFar && tlsNegotiateErr == nil
 
 	webhookAuthenticator, conditions, err := newWebhookAuthenticator(
-		&obj.Spec,
+		specCopy.Endpoint,
+		pemBytes,
 		os.CreateTemp,
 		clientcmd.WriteToFile,
 		conditions,
@@ -151,7 +152,8 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 // newWebhookAuthenticator creates a webhook from the provided API server url and caBundle
 // used to validate TLS connections.
 func newWebhookAuthenticator(
-	spec *auth1alpha1.WebhookAuthenticatorSpec,
+	endpoint string,
+	pemBytes []byte,
 	tempfileFunc func(string, string) (*os.File, error),
 	marshalFunc func(clientcmdapi.Config, string) error,
 	conditions []*metav1.Condition,
@@ -180,19 +182,8 @@ func newWebhookAuthenticator(
 	}
 	defer func() { _ = os.Remove(temp.Name()) }()
 
-	cluster := &clientcmdapi.Cluster{Server: spec.Endpoint}
-	_, cluster.CertificateAuthorityData, err = pinnipedauthenticator.CABundle(spec.TLS)
-	if err != nil {
-		errText := "invalid TLS configuration"
-		msg := fmt.Sprintf("%s: %s", errText, err.Error())
-		conditions = append(conditions, &metav1.Condition{
-			Type:    typeAuthenticatorValid,
-			Status:  metav1.ConditionFalse,
-			Reason:  reasonInvalidTLSConfiguration,
-			Message: msg,
-		})
-		return nil, conditions, fmt.Errorf("%s: %w", errText, err)
-	}
+	cluster := &clientcmdapi.Cluster{Server: endpoint}
+	cluster.CertificateAuthorityData = pemBytes
 
 	kubeconfig := clientcmdapi.NewConfig()
 	kubeconfig.Clusters["anonymous-cluster"] = cluster
@@ -222,7 +213,7 @@ func newWebhookAuthenticator(
 
 	// We set this to nil because we would only need this to support some of the
 	// custom proxy stuff used by the API server.
-	var customDial net.DialFunc
+	var customDial k8snetutil.DialFunc
 
 	// TODO refactor this code to directly construct the rest.Config
 	//  ideally we would keep rest config generation contained to the kubeclient package
@@ -242,6 +233,7 @@ func newWebhookAuthenticator(
 	//  then use client.JSONConfig as clientConfig
 	clientConfig, err := webhookutil.LoadKubeconfig(temp.Name(), customDial)
 	if err != nil {
+		// no unit test for this failure.
 		errText := "unable to load kubeconfig"
 		msg := fmt.Sprintf("%s: %s", errText, err.Error())
 		conditions = append(conditions, &metav1.Condition{
@@ -258,6 +250,7 @@ func newWebhookAuthenticator(
 	//   NOTE: looks like the above was merged on Mar 18, 2022
 	webhookA, err := webhook.New(clientConfig, version, implicitAuds, *webhook.DefaultRetryBackoff())
 	if err != nil {
+		// no unit test for this failure.
 		errText := "unable to instantiate webhook"
 		msg := fmt.Sprintf("%s: %s", errText, err.Error())
 		conditions = append(conditions, &metav1.Condition{
@@ -330,8 +323,8 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer
 	return conditions, nil
 }
 
-func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
-	rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec)
+func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []byte, []*metav1.Condition, bool) {
+	rootCAs, pemBytes, err := pinnipedauthenticator.CABundle(tlsSpec)
 	if err != nil {
 		msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
 		conditions = append(conditions, &metav1.Condition{
@@ -340,7 +333,7 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL
 			Reason:  reasonInvalidTLSConfiguration,
 			Message: msg,
 		})
-		return rootCAs, conditions, false
+		return rootCAs, pemBytes, conditions, false
 	}
 	msg := "successfully parsed specified CA bundle"
 	if rootCAs == nil {
@@ -352,7 +345,7 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL
 		Reason:  reasonSuccess,
 		Message: msg,
 	})
-	return rootCAs, conditions, true
+	return rootCAs, pemBytes, conditions, true
 }
 
 func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditions []*metav1.Condition) (*url.URL, []*metav1.Condition, bool) {
@@ -395,7 +388,7 @@ func (c *webhookCacheFillerController) updateStatus(
 ) error {
 	updated := original.DeepCopy()
 
-	if hadErrorCondition(conditions) {
+	if conditionsutil.HadErrorCondition(conditions) {
 		updated.Status.Phase = auth1alpha1.WebhookAuthenticatorPhaseError
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeReady,
@@ -431,12 +424,3 @@ func (c *webhookCacheFillerController) updateStatus(
 	}
 	return err
 }
-
-func hadErrorCondition(conditions []*metav1.Condition) bool {
-	for _, c := range conditions {
-		if c.Status != metav1.ConditionTrue {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index 4cef02969..a786e2f39 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -395,9 +395,6 @@ func TestController(t *testing.T) {
 			},
 			wantCacheEntries: 0,
 		},
-		// Existing code that was never tested. We would likely have to create a server with bad clients to
-		// simulate this.
-		// { name: "non-404 `failed to get webhook authenticator` for other API server reasons" }
 		{
 			name:    "Sync: valid and unchanged WebhookAuthenticator: loop will preserve existing status conditions",
 			syncKey: controllerlib.Key{Name: "test-name"},
@@ -443,10 +440,10 @@ func TestController(t *testing.T) {
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 666),
 							[]metav1.Condition{
-								sadReadyCondition(frozenTimeInThePast, 0),
-								happyEndpointURLValid(frozenTimeInThePast, 0),
+								sadReadyCondition(frozenTimeInThePast, 777),
+								happyEndpointURLValid(frozenTimeInThePast, 888),
 							},
 						),
 						Phase: "Ready",
@@ -1119,7 +1116,7 @@ func TestController(t *testing.T) {
 					require.Fail(t, cmp.Diff(tt.wantActions(), pinnipedAPIClient.Actions()), "actions should be exactly the expected number of actions and also contain the correct resources")
 				}
 			} else {
-				require.Error(t, errors.New("wantActions is required for test "+tt.name))
+				require.Fail(t, "wantActions is required for test "+tt.name)
 			}
 
 			require.Equal(t, tt.wantCacheEntries, len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", tt.wantCacheEntries, len(cache.Keys()), cache.Keys()))
@@ -1132,7 +1129,7 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 
 	t.Run("prerequisites not ready, cannot create webhook authenticator", func(t *testing.T) {
 		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{}, os.CreateTemp, clientcmd.WriteToFile, conditions, false)
+		res, conditions, err := newWebhookAuthenticator("", []byte("some pem bytes"), os.CreateTemp, clientcmd.WriteToFile, conditions, false)
 		require.Equal(t, []*metav1.Condition{
 			{
 				Type:    "AuthenticatorValid",
@@ -1142,13 +1139,13 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 			},
 		}, conditions)
 		require.Nil(t, res)
-		require.Nil(t, err)
+		require.NoError(t, err)
 	})
 
 	t.Run("temp file failure, cannot create webhook authenticator", func(t *testing.T) {
 		brokenTempFile := func(_ string, _ string) (*os.File, error) { return nil, fmt.Errorf("some temp file error") }
 		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(nil, brokenTempFile, clientcmd.WriteToFile, conditions, true)
+		res, conditions, err := newWebhookAuthenticator("", []byte("some pem bytes"), brokenTempFile, clientcmd.WriteToFile, conditions, true)
 		require.Equal(t, []*metav1.Condition{
 			{
 				Type:    "AuthenticatorValid",
@@ -1164,7 +1161,7 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 	t.Run("marshal failure, cannot create webhook authenticator", func(t *testing.T) {
 		marshalError := func(_ clientcmdapi.Config, _ string) error { return fmt.Errorf("some marshal error") }
 		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{}, os.CreateTemp, marshalError, conditions, true)
+		res, conditions, err := newWebhookAuthenticator("", []byte("some pem bytes"), os.CreateTemp, marshalError, conditions, true)
 		require.Equal(t, []*metav1.Condition{
 			{
 				Type:    "AuthenticatorValid",
@@ -1177,49 +1174,24 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 		require.EqualError(t, err, "unable to marshal kubeconfig: some marshal error")
 	})
 
-	// t.Run("load kubeconfig err, not currently tested, may not be necessary to test?")
-
-	t.Run("invalid TLS config, base64 encoding err, cannot create webhook authenticator", func(t *testing.T) {
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: goodEndpoint,
-			TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid-base64"},
-		}, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
-		require.Equal(t, []*metav1.Condition{
-			{
-				Type:    "AuthenticatorValid",
-				Status:  "False",
-				Reason:  "InvalidTLSConfiguration",
-				Message: "invalid TLS configuration: illegal base64 data at input byte 7",
-			},
-		}, conditions)
-		require.Nil(t, res)
-		require.EqualError(t, err, "invalid TLS configuration: illegal base64 data at input byte 7")
-	})
-
-	t.Run("invalid pem data, cannot create webhook authenticator", func(t *testing.T) {
+	t.Run("invalid pem data, unable to parse bytes as PEM block", func(t *testing.T) {
 		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: goodEndpoint,
-			TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte("bad data"))},
-		}, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
+		res, conditions, err := newWebhookAuthenticator(goodEndpoint, []byte("invalid-bas64"), os.CreateTemp, clientcmd.WriteToFile, conditions, true)
 		require.Equal(t, []*metav1.Condition{
 			{
 				Type:    "AuthenticatorValid",
 				Status:  "False",
-				Reason:  "InvalidTLSConfiguration",
-				Message: "invalid TLS configuration: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates",
+				Reason:  "UnableToInstantiateWebhook",
+				Message: "unable to instantiate webhook: unable to load root certificates: unable to parse bytes as PEM block",
 			},
 		}, conditions)
 		require.Nil(t, res)
-		require.EqualError(t, err, "invalid TLS configuration: certificateAuthorityData is not valid PEM: data does not contain any valid RSA or ECDSA certificates")
+		require.EqualError(t, err, "unable to instantiate webhook: unable to load root certificates: unable to parse bytes as PEM block")
 	})
 
 	t.Run("valid config with no TLS spec, webhook authenticator created", func(t *testing.T) {
 		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(&auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: goodEndpoint,
-		}, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
+		res, conditions, err := newWebhookAuthenticator(goodEndpoint, nil, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
 		require.Equal(t, []*metav1.Condition{
 			{
 				Type:    "AuthenticatorValid",
@@ -1240,14 +1212,8 @@ func TestNewWebhookAuthenticator(t *testing.T) {
 			_, err = w.Write([]byte(`{}`))
 			require.NoError(t, err)
 		})
-		spec := &auth1alpha1.WebhookAuthenticatorSpec{
-			Endpoint: url,
-			TLS: &auth1alpha1.TLSSpec{
-				CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(caBundle)),
-			},
-		}
 		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(spec, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
+		res, conditions, err := newWebhookAuthenticator(url, []byte(caBundle), os.CreateTemp, clientcmd.WriteToFile, conditions, true)
 		require.NoError(t, err)
 		require.NotNil(t, res)
 		require.Equal(t, []*metav1.Condition{

From 90e7343fb58477d537fc3527056dce6f1850db83 Mon Sep 17 00:00:00 2001
From: Joshua Casey <joshuatcasey@gmail.com>
Date: Tue, 19 Mar 2024 12:31:11 -0500
Subject: [PATCH 10/17] Add IPv6 test to WebhookAuthenticator ctrl tests

Co-authored-by: Benjamin A. Petersen <ben@benjaminapetersen.me>
---
 .../webhookcachefiller_test.go                | 164 ++++++++++++------
 1 file changed, 113 insertions(+), 51 deletions(-)

diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index a786e2f39..8aa3a6e55 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -117,7 +117,7 @@ func TestController(t *testing.T) {
 	})
 
 	localWithExampleDotComMux := http.NewServeMux()
-	localWithExampleDotComCertServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	hostLocalWithExampleDotComCertServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		tlsserver.AssertTLS(t, r, ptls.Default)
 		localWithExampleDotComMux.ServeHTTP(w, r)
 	}), func(thisServer *httptest.Server) {
@@ -129,7 +129,7 @@ func TestController(t *testing.T) {
 	})
 
 	goodMux := http.NewServeMux()
-	goodWebhookServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+	hostGoodDefaultServingCertServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		tlsserver.AssertTLS(t, r, ptls.Default)
 		goodMux.ServeHTTP(w, r)
 	}), tlsserver.RecordTLSHello)
@@ -142,14 +142,17 @@ func TestController(t *testing.T) {
 		w.WriteHeader(http.StatusNotFound)
 		fmt.Fprint(w, "404 nothing here")
 	}))
-	goodEndpoint := goodWebhookServer.URL
-	goodEndpointBut404 := goodEndpoint + "/nothing/here"
+
+	goodWebhookDefaultServingCertEndpoint := hostGoodDefaultServingCertServer.URL
+	goodWebhookDefaultServingCertEndpointBut404 := goodWebhookDefaultServingCertEndpoint + "/nothing/here"
 
 	localhostURL, err := url.Parse(hostAsLocalhostWebhookServer.URL)
 	require.NoError(t, err)
-	localhostEndpointURL := fmt.Sprintf("%s:%s", "https://localhost", localhostURL.Port())
 
-	badEndpoint := "https://.café   .com/café/café/café/coffee"
+	hostAs127001WebhookServerURL, err := url.Parse(hostAs127001WebhookServer.URL)
+	require.NoError(t, err)
+
+	badEndpointInvalidURL := "https://.café   .com/café/café/café/coffee"
 	badEndpointNoHTTPS := "http://localhost"
 
 	nowDoesntMatter := time.Date(1122, time.September, 33, 4, 55, 56, 778899, time.Local)
@@ -160,39 +163,32 @@ func TestController(t *testing.T) {
 	frozenTimeInThePast := metav1.NewTime(timeInThePast)
 
 	goodWebhookAuthenticatorSpecWithCA := auth1alpha1.WebhookAuthenticatorSpec{
-		Endpoint: goodEndpoint,
-		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodWebhookServer.TLS),
-	}
-	localhostWebhookAuthenticatorSpecWithCA := auth1alpha1.WebhookAuthenticatorSpec{
-		Endpoint: localhostEndpointURL,
-		TLS: &auth1alpha1.TLSSpec{
-			// CA Bundle for validating the server's certs
-			CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAsHostname.Bundle()),
-		},
+		Endpoint: goodWebhookDefaultServingCertEndpoint,
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(hostGoodDefaultServingCertServer.TLS),
 	}
 	localWithExampleDotComWeebhookAuthenticatorSpec := auth1alpha1.WebhookAuthenticatorSpec{
 		// CA for example.com, TLS serving cert for example.com, but endpoint is still localhost
-		Endpoint: localWithExampleDotComCertServer.URL,
+		Endpoint: hostLocalWithExampleDotComCertServer.URL,
 		TLS: &auth1alpha1.TLSSpec{
 			// CA Bundle for example.com
 			CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForExampleDotCom.Bundle()),
 		},
 	}
 	goodWebhookAuthenticatorSpecWithoutCA := auth1alpha1.WebhookAuthenticatorSpec{
-		Endpoint: goodEndpoint,
+		Endpoint: goodWebhookDefaultServingCertEndpoint,
 		TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: ""},
 	}
 	goodWebhookAuthenticatorSpecWith404Endpoint := auth1alpha1.WebhookAuthenticatorSpec{
-		Endpoint: goodEndpointBut404,
-		TLS:      conciergetestutil.TLSSpecFromTLSConfig(goodWebhookServer.TLS),
+		Endpoint: goodWebhookDefaultServingCertEndpointBut404,
+		TLS:      conciergetestutil.TLSSpecFromTLSConfig(hostGoodDefaultServingCertServer.TLS),
 	}
 	badWebhookAuthenticatorSpecInvalidTLS := auth1alpha1.WebhookAuthenticatorSpec{
-		Endpoint: goodEndpoint,
+		Endpoint: goodWebhookDefaultServingCertEndpoint,
 		TLS:      &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"},
 	}
 
 	badWebhookAuthenticatorSpecGoodEndpointButUnknownCA := auth1alpha1.WebhookAuthenticatorSpec{
-		Endpoint: goodEndpoint,
+		Endpoint: goodWebhookDefaultServingCertEndpoint,
 		TLS: &auth1alpha1.TLSSpec{
 			CertificateAuthorityData: base64.StdEncoding.EncodeToString(pemServerCertForUnknownServer),
 		},
@@ -405,7 +401,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				},
@@ -416,7 +412,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodWebhookServer.URL,
+					"endpoint":  goodWebhookDefaultServingCertEndpoint,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -440,7 +436,7 @@ func TestController(t *testing.T) {
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 666),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 666),
 							[]metav1.Condition{
 								sadReadyCondition(frozenTimeInThePast, 777),
 								happyEndpointURLValid(frozenTimeInThePast, 888),
@@ -456,7 +452,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodWebhookServer.URL,
+					"endpoint":  goodWebhookDefaultServingCertEndpoint,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -470,7 +466,7 @@ func TestController(t *testing.T) {
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								happyEndpointURLValid(frozenTimeInThePast, 0),
 							},
@@ -503,7 +499,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodWebhookServer.URL,
+					"endpoint":  goodWebhookDefaultServingCertEndpoint,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -516,7 +512,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				})
@@ -548,7 +544,7 @@ func TestController(t *testing.T) {
 					Spec: goodWebhookAuthenticatorSpecWithoutCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadTLSConnectionNegotiationValid(frozenMetav1Now, 0),
@@ -588,7 +584,7 @@ func TestController(t *testing.T) {
 					Spec: badWebhookAuthenticatorSpecInvalidTLS,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								sadTLSConfigurationValid(frozenMetav1Now, 0),
 								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
@@ -617,7 +613,7 @@ func TestController(t *testing.T) {
 						Name: "test-name",
 					},
 					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: badEndpoint,
+						Endpoint: badEndpointInvalidURL,
 					},
 				},
 			},
@@ -627,11 +623,11 @@ func TestController(t *testing.T) {
 						Name: "test-name",
 					},
 					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: badEndpoint,
+						Endpoint: badEndpointInvalidURL,
 					},
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValid("https://.café   .com/café/café/café/coffee", frozenMetav1Now, 0),
@@ -674,7 +670,7 @@ func TestController(t *testing.T) {
 					},
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValidHTTPS("http://localhost", frozenMetav1Now, 0),
@@ -715,7 +711,7 @@ func TestController(t *testing.T) {
 					Spec: badWebhookAuthenticatorSpecGoodEndpointButUnknownCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
@@ -754,7 +750,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodEndpointBut404,
+					"endpoint":  goodWebhookDefaultServingCertEndpointBut404,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -767,7 +763,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: goodWebhookAuthenticatorSpecWith404Endpoint,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(goodEndpointBut404, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpointBut404, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				})
@@ -780,16 +776,22 @@ func TestController(t *testing.T) {
 			},
 			wantCacheEntries: 1,
 		}, {
-			name:    "validateTLSNegotiation: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
+			name:    "validateTLSNegotiation: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
 					ObjectMeta: metav1.ObjectMeta{
 						Name: "test-name",
 					},
-					Spec: localhostWebhookAuthenticatorSpecWithCA,
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: fmt.Sprintf("%s:%s", "https://localhost", localhostURL.Port()),
+						TLS: &auth1alpha1.TLSSpec{
+							// CA Bundle for validating the server's certs
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAsHostname.Bundle()),
+						},
+					},
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(localhostEndpointURL, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(fmt.Sprintf("%s:%s", "https://localhost", localhostURL.Port()), frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				},
@@ -800,7 +802,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  localhostEndpointURL,
+					"endpoint":  fmt.Sprintf("%s:%s", "https://localhost", localhostURL.Port()),
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -813,6 +815,66 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
+		}, {
+			name:    "validateTLSNegotiation: localhost IPv6 address instead of 127.0.0.1 should still dial correctly as dialer should handle addresses",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
+				// We are dialing a different server here since CI doesn't have the linux IPv6 stack installed.
+				// This test proves that IPv6 addresses are parsed & handled correctly before we call tls.Dial in production code.
+				return tls.Dial(network, hostAs127001WebhookServerURL.Host, &tls.Config{
+					MinVersion: tls.VersionTLS12,
+					RootCAs:    caForLocalhostAs127001.Pool(),
+				})
+			},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()),
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+				},
+			},
+			wantLogs: []map[string]any{
+				{
+					"level":     "info",
+					"timestamp": "2099-08-08T13:57:36.123456Z",
+					"logger":    "webhookcachefiller-controller",
+					"message":   "added new webhook authenticator",
+					"endpoint":  fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()),
+					"webhook": map[string]interface{}{
+						"name": "test-name",
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()),
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: allHappyConditionsSuccess(fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()), frozenMetav1Now, 0),
+						Phase:      "Ready",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 1,
 		}, {
 			name:    "validateTLSNegotiation: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
 			syncKey: controllerlib.Key{Name: "test-name"},
@@ -862,7 +924,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: localWithExampleDotComWeebhookAuthenticatorSpec,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(localWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(hostLocalWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				},
@@ -875,7 +937,7 @@ func TestController(t *testing.T) {
 					Spec: localWithExampleDotComWeebhookAuthenticatorSpec,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(localWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(hostLocalWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								sadTLSConnectionNegotiationNoIPSANs(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
@@ -904,7 +966,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				},
@@ -915,7 +977,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodWebhookServer.URL,
+					"endpoint":  goodWebhookDefaultServingCertEndpoint,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -939,7 +1001,7 @@ func TestController(t *testing.T) {
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -954,7 +1016,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodWebhookServer.URL,
+					"endpoint":  goodWebhookDefaultServingCertEndpoint,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -967,7 +1029,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				})
@@ -999,7 +1061,7 @@ func TestController(t *testing.T) {
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -1014,7 +1076,7 @@ func TestController(t *testing.T) {
 					"timestamp": "2099-08-08T13:57:36.123456Z",
 					"logger":    "webhookcachefiller-controller",
 					"message":   "added new webhook authenticator",
-					"endpoint":  goodWebhookServer.URL,
+					"endpoint":  goodWebhookDefaultServingCertEndpoint,
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
@@ -1032,7 +1094,7 @@ func TestController(t *testing.T) {
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(goodEndpoint, frozenMetav1Now, 0),
+						Conditions: allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 						Phase:      "Ready",
 					},
 				})

From 5bc4e678bf1f7a8dcfcffeabb16cdc40b7bb6654 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Tue, 19 Mar 2024 17:45:26 -0400
Subject: [PATCH 11/17] WebhookAuthenticator Status integration test refactor
 to test table

---
 ...cierge_webhookauthenticator_status_test.go | 216 ++++++++----------
 1 file changed, 99 insertions(+), 117 deletions(-)

diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index 70a139e98..e5aa0fe7e 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -21,157 +21,139 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
 	t.Cleanup(cancel)
 
+	caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
+
 	tests := []struct {
-		name string
-		run  func(t *testing.T)
+		name            string
+		spec            func() *v1alpha1.WebhookAuthenticatorSpec
+		initialPhase    v1alpha1.WebhookAuthenticatorPhase
+		finalConditions []metav1.Condition
+		run             func(t *testing.T)
 	}{
 		{
 			name: "Basic test to see if the WebhookAuthenticator wakes up or not.",
-			run: func(t *testing.T) {
-				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
-					ctx,
-					t,
-					&testlib.IntegrationEnv(t).TestWebhook,
-					v1alpha1.WebhookAuthenticatorPhaseReady)
-
-				testlib.WaitForWebhookAuthenticatorStatusConditions(
-					ctx, t,
-					webhookAuthenticator.Name,
-					allSuccessfulWebhookAuthenticatorConditions())
+			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
+				return &testlib.IntegrationEnv(t).TestWebhook
 			},
+			initialPhase:    v1alpha1.WebhookAuthenticatorPhaseReady,
+			finalConditions: allSuccessfulWebhookAuthenticatorConditions(),
 		}, {
 			name: "valid spec with invalid CA in TLS config will result in a WebhookAuthenticator that is not ready",
-			run: func(t *testing.T) {
+			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
 				caBundleString := "invalid base64-encoded data"
 				webhookSpec := testEnv.TestWebhook.DeepCopy()
 				webhookSpec.TLS = &v1alpha1.TLSSpec{
 					CertificateAuthorityData: caBundleString,
 				}
-
-				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
-					ctx,
-					t,
-					webhookSpec,
-					v1alpha1.WebhookAuthenticatorPhaseError)
-
-				testlib.WaitForWebhookAuthenticatorStatusConditions(
-					ctx, t,
-					webhookAuthenticator.Name,
-					replaceSomeConditions(
-						allSuccessfulWebhookAuthenticatorConditions(),
-						[]metav1.Condition{
-							{
-								Type:    "Ready",
-								Status:  "False",
-								Reason:  "NotReady",
-								Message: "the WebhookAuthenticator is not ready: see other conditions for details",
-							}, {
-								Type:    "AuthenticatorValid",
-								Status:  "Unknown",
-								Reason:  "UnableToValidate",
-								Message: "unable to validate; see other conditions for details",
-							}, {
-								Type:    "TLSConfigurationValid",
-								Status:  "False",
-								Reason:  "InvalidTLSConfiguration",
-								Message: "invalid TLS configuration: illegal base64 data at input byte 7",
-							}, {
-								Type:    "TLSConnectionNegotiationValid",
-								Status:  "Unknown",
-								Reason:  "UnableToValidate",
-								Message: "unable to validate; see other conditions for details",
-							},
-						},
-					))
+				return webhookSpec
 			},
+			initialPhase: v1alpha1.WebhookAuthenticatorPhaseError,
+			finalConditions: replaceSomeConditions(
+				allSuccessfulWebhookAuthenticatorConditions(),
+				[]metav1.Condition{
+					{
+						Type:    "Ready",
+						Status:  "False",
+						Reason:  "NotReady",
+						Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+					}, {
+						Type:    "AuthenticatorValid",
+						Status:  "Unknown",
+						Reason:  "UnableToValidate",
+						Message: "unable to validate; see other conditions for details",
+					}, {
+						Type:    "TLSConfigurationValid",
+						Status:  "False",
+						Reason:  "InvalidTLSConfiguration",
+						Message: "invalid TLS configuration: illegal base64 data at input byte 7",
+					}, {
+						Type:    "TLSConnectionNegotiationValid",
+						Status:  "Unknown",
+						Reason:  "UnableToValidate",
+						Message: "unable to validate; see other conditions for details",
+					},
+				},
+			),
 		}, {
 			name: "valid spec with valid CA in TLS config but does not match issuer server will result in a WebhookAuthenticator that is not ready",
-			run: func(t *testing.T) {
-				caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
+			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
 				webhookSpec := testEnv.TestWebhook.DeepCopy()
 				webhookSpec.TLS = &v1alpha1.TLSSpec{
 					CertificateAuthorityData: caBundleSomePivotalCA,
 				}
-
-				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
-					ctx,
-					t,
-					webhookSpec,
-					v1alpha1.WebhookAuthenticatorPhaseError)
-
-				testlib.WaitForWebhookAuthenticatorStatusConditions(
-					ctx, t,
-					webhookAuthenticator.Name,
-					replaceSomeConditions(
-						allSuccessfulWebhookAuthenticatorConditions(),
-						[]metav1.Condition{
-							{
-								Type:    "Ready",
-								Status:  "False",
-								Reason:  "NotReady",
-								Message: "the WebhookAuthenticator is not ready: see other conditions for details",
-							}, {
-								Type:    "AuthenticatorValid",
-								Status:  "Unknown",
-								Reason:  "UnableToValidate",
-								Message: "unable to validate; see other conditions for details",
-							}, {
-								Type:    "TLSConnectionNegotiationValid",
-								Status:  "False",
-								Reason:  "UnableToDialServer",
-								Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
-							},
-						},
-					))
+				return webhookSpec
 			},
+			initialPhase: v1alpha1.WebhookAuthenticatorPhaseError,
+			finalConditions: replaceSomeConditions(
+				allSuccessfulWebhookAuthenticatorConditions(),
+				[]metav1.Condition{
+					{
+						Type:    "Ready",
+						Status:  "False",
+						Reason:  "NotReady",
+						Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+					}, {
+						Type:    "AuthenticatorValid",
+						Status:  "Unknown",
+						Reason:  "UnableToValidate",
+						Message: "unable to validate; see other conditions for details",
+					}, {
+						Type:    "TLSConnectionNegotiationValid",
+						Status:  "False",
+						Reason:  "UnableToDialServer",
+						Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
+					},
+				},
+			),
 		}, {
 			name: "invalid with unresponsive endpoint will result in a WebhookAuthenticator that is not ready",
-			run: func(t *testing.T) {
-				caBundleSomePivotalCA := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
+			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
 				webhookSpec := testEnv.TestWebhook.DeepCopy()
 				webhookSpec.TLS = &v1alpha1.TLSSpec{
 					CertificateAuthorityData: caBundleSomePivotalCA,
 				}
 				webhookSpec.Endpoint = "https://127.0.0.1:443/some-fake-endpoint"
-
-				webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
-					ctx,
-					t,
-					webhookSpec,
-					v1alpha1.WebhookAuthenticatorPhaseError)
-
-				testlib.WaitForWebhookAuthenticatorStatusConditions(
-					ctx, t,
-					webhookAuthenticator.Name,
-					replaceSomeConditions(
-						allSuccessfulWebhookAuthenticatorConditions(),
-						[]metav1.Condition{
-							{
-								Type:    "Ready",
-								Status:  "False",
-								Reason:  "NotReady",
-								Message: "the WebhookAuthenticator is not ready: see other conditions for details",
-							}, {
-								Type:    "AuthenticatorValid",
-								Status:  "Unknown",
-								Reason:  "UnableToValidate",
-								Message: "unable to validate; see other conditions for details",
-							}, {
-								Type:    "TLSConnectionNegotiationValid",
-								Status:  "False",
-								Reason:  "UnableToDialServer",
-								Message: "cannot dial server: dial tcp 127.0.0.1:443: connect: connection refused",
-							},
-						},
-					))
+				return webhookSpec
 			},
+			initialPhase: v1alpha1.WebhookAuthenticatorPhaseError,
+			finalConditions: replaceSomeConditions(
+				allSuccessfulWebhookAuthenticatorConditions(),
+				[]metav1.Condition{
+					{
+						Type:    "Ready",
+						Status:  "False",
+						Reason:  "NotReady",
+						Message: "the WebhookAuthenticator is not ready: see other conditions for details",
+					}, {
+						Type:    "AuthenticatorValid",
+						Status:  "Unknown",
+						Reason:  "UnableToValidate",
+						Message: "unable to validate; see other conditions for details",
+					}, {
+						Type:    "TLSConnectionNegotiationValid",
+						Status:  "False",
+						Reason:  "UnableToDialServer",
+						Message: "cannot dial server: dial tcp 127.0.0.1:443: connect: connection refused",
+					},
+				},
+			),
 		},
 	}
 	for _, test := range tests {
 		tt := test
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
-			tt.run(t)
+
+			webhookAuthenticator := testlib.CreateTestWebhookAuthenticator(
+				ctx,
+				t,
+				tt.spec(),
+				tt.initialPhase)
+
+			testlib.WaitForWebhookAuthenticatorStatusConditions(
+				ctx, t,
+				webhookAuthenticator.Name,
+				tt.finalConditions)
 		})
 	}
 }

From bec5fe85cc985f87bb6bde4b91cd297e55dbc01e Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Tue, 19 Mar 2024 18:00:40 -0400
Subject: [PATCH 12/17] change WebhookAuthenticator
 TLSConnectionNegotiationValid to ConnectionProbeValid

---
 .../webhookcachefiller/webhookcachefiller.go  | 42 +++++++++----------
 .../webhookcachefiller_test.go                | 30 ++++++-------
 ...cierge_webhookauthenticator_status_test.go | 16 +++----
 3 files changed, 44 insertions(+), 44 deletions(-)

diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index f5f8a3123..b96c0c754 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -39,24 +39,24 @@ import (
 )
 
 const (
-	controllerName                    = "webhookcachefiller-controller"
-	typeReady                         = "Ready"
-	typeTLSConfigurationValid         = "TLSConfigurationValid"
-	typeTLSConnectionNegotiationValid = "TLSConnectionNegotiationValid"
-	typeEndpointURLValid              = "EndpointURLValid"
-	typeAuthenticatorValid            = "AuthenticatorValid"
-	reasonSuccess                     = "Success"
-	reasonNotReady                    = "NotReady"
-	reasonUnableToValidate            = "UnableToValidate"
-	reasonUnableToCreateTempFile      = "UnableToCreateTempFile"
-	reasonUnableToMarshallKubeconfig  = "UnableToMarshallKubeconfig"
-	reasonUnableToLoadKubeconfig      = "UnableToLoadKubeconfig"
-	reasonUnableToInstantiateWebhook  = "UnableToInstantiateWebhook"
-	reasonInvalidTLSConfiguration     = "InvalidTLSConfiguration"
-	reasonInvalidEndpointURL          = "InvalidEndpointURL"
-	reasonInvalidEndpointURLScheme    = "InvalidEndpointURLScheme"
-	reasonUnableToDialServer          = "UnableToDialServer"
-	msgUnableToValidate               = "unable to validate; see other conditions for details"
+	controllerName                   = "webhookcachefiller-controller"
+	typeReady                        = "Ready"
+	typeTLSConfigurationValid        = "TLSConfigurationValid"
+	typeConnectionProbeValid         = "ConnectionProbeValid"
+	typeEndpointURLValid             = "EndpointURLValid"
+	typeAuthenticatorValid           = "AuthenticatorValid"
+	reasonSuccess                    = "Success"
+	reasonNotReady                   = "NotReady"
+	reasonUnableToValidate           = "UnableToValidate"
+	reasonUnableToCreateTempFile     = "UnableToCreateTempFile"
+	reasonUnableToMarshallKubeconfig = "UnableToMarshallKubeconfig"
+	reasonUnableToLoadKubeconfig     = "UnableToLoadKubeconfig"
+	reasonUnableToInstantiateWebhook = "UnableToInstantiateWebhook"
+	reasonInvalidTLSConfiguration    = "InvalidTLSConfiguration"
+	reasonInvalidEndpointURL         = "InvalidEndpointURL"
+	reasonInvalidEndpointURLScheme   = "InvalidEndpointURLScheme"
+	reasonUnableToDialServer         = "UnableToDialServer"
+	msgUnableToValidate              = "unable to validate; see other conditions for details"
 )
 
 // New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
@@ -274,7 +274,7 @@ func newWebhookAuthenticator(
 func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.CertPool, endpointURL *url.URL, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
 	if !prereqOk {
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeTLSConnectionNegotiationValid,
+			Type:    typeConnectionProbeValid,
 			Status:  metav1.ConditionUnknown,
 			Reason:  reasonUnableToValidate,
 			Message: msgUnableToValidate,
@@ -300,7 +300,7 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer
 		errText := "cannot dial server"
 		msg := fmt.Sprintf("%s: %s", errText, dialErr.Error())
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeTLSConnectionNegotiationValid,
+			Type:    typeConnectionProbeValid,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonUnableToDialServer,
 			Message: msg,
@@ -315,7 +315,7 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer
 	}
 
 	conditions = append(conditions, &metav1.Condition{
-		Type:    typeTLSConnectionNegotiationValid,
+		Type:    typeConnectionProbeValid,
 		Status:  metav1.ConditionTrue,
 		Reason:  reasonSuccess,
 		Message: "tls verified",
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index 8aa3a6e55..4d4b4c2a3 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -267,9 +267,9 @@ func TestController(t *testing.T) {
 		}
 	}
 
-	happyTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	happyConnectionProbeValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnectionNegotiationValid",
+			Type:               "ConnectionProbeValid",
 			Status:             "True",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -277,9 +277,9 @@ func TestController(t *testing.T) {
 			Message:            "tls verified",
 		}
 	}
-	unknownTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	unknownConnectionProbeValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnectionNegotiationValid",
+			Type:               "ConnectionProbeValid",
 			Status:             "Unknown",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -287,9 +287,9 @@ func TestController(t *testing.T) {
 			Message:            "unable to validate; see other conditions for details",
 		}
 	}
-	sadTLSConnectionNegotiationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadConnectionProbeValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnectionNegotiationValid",
+			Type:               "ConnectionProbeValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -297,9 +297,9 @@ func TestController(t *testing.T) {
 			Message:            "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
 		}
 	}
-	sadTLSConnectionNegotiationNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadConnectionProbeValidNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "TLSConnectionNegotiationValid",
+			Type:               "ConnectionProbeValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -343,7 +343,7 @@ func TestController(t *testing.T) {
 		return conditionstestutil.SortByType([]metav1.Condition{
 			happyTLSConfigurationValidCAParsed(someTime, observedGeneration),
 			happyEndpointURLValid(someTime, observedGeneration),
-			happyTLSConnectionNegotiationValid(someTime, observedGeneration),
+			happyConnectionProbeValid(someTime, observedGeneration),
 			happyAuthenticatorValid(someTime, observedGeneration),
 			happyReadyCondition(someTime, observedGeneration),
 		})
@@ -547,7 +547,7 @@ func TestController(t *testing.T) {
 							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
-								sadTLSConnectionNegotiationValid(frozenMetav1Now, 0),
+								sadConnectionProbeValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
@@ -587,7 +587,7 @@ func TestController(t *testing.T) {
 							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								sadTLSConfigurationValid(frozenMetav1Now, 0),
-								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
+								unknownConnectionProbeValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -631,7 +631,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValid("https://.café   .com/café/café/café/coffee", frozenMetav1Now, 0),
-								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
+								unknownConnectionProbeValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -674,7 +674,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValidHTTPS("http://localhost", frozenMetav1Now, 0),
-								unknownTLSConnectionNegotiationValid(frozenMetav1Now, 0),
+								unknownConnectionProbeValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -715,7 +715,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
-								sadTLSConnectionNegotiationValid(frozenMetav1Now, 0),
+								sadConnectionProbeValid(frozenMetav1Now, 0),
 							},
 						),
 						Phase: "Error",
@@ -939,7 +939,7 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(hostLocalWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadTLSConnectionNegotiationNoIPSANs(frozenMetav1Now, 0),
+								sadConnectionProbeValidNoIPSANs(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index e5aa0fe7e..fd9893428 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -67,7 +67,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 						Reason:  "InvalidTLSConfiguration",
 						Message: "invalid TLS configuration: illegal base64 data at input byte 7",
 					}, {
-						Type:    "TLSConnectionNegotiationValid",
+						Type:    "ConnectionProbeValid",
 						Status:  "Unknown",
 						Reason:  "UnableToValidate",
 						Message: "unable to validate; see other conditions for details",
@@ -98,7 +98,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 						Reason:  "UnableToValidate",
 						Message: "unable to validate; see other conditions for details",
 					}, {
-						Type:    "TLSConnectionNegotiationValid",
+						Type:    "ConnectionProbeValid",
 						Status:  "False",
 						Reason:  "UnableToDialServer",
 						Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
@@ -130,7 +130,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 						Reason:  "UnableToValidate",
 						Message: "unable to validate; see other conditions for details",
 					}, {
-						Type:    "TLSConnectionNegotiationValid",
+						Type:    "ConnectionProbeValid",
 						Status:  "False",
 						Reason:  "UnableToDialServer",
 						Message: "cannot dial server: dial tcp 127.0.0.1:443: connect: connection refused",
@@ -266,6 +266,11 @@ func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition {
 		Status:  "True",
 		Reason:  "Success",
 		Message: "authenticator initialized",
+	}, {
+		Type:    "ConnectionProbeValid",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "tls verified",
 	}, {
 		Type:    "EndpointURLValid",
 		Status:  "True",
@@ -281,10 +286,5 @@ func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition {
 		Status:  "True",
 		Reason:  "Success",
 		Message: "successfully parsed specified CA bundle",
-	}, {
-		Type:    "TLSConnectionNegotiationValid",
-		Status:  "True",
-		Reason:  "Success",
-		Message: "tls verified",
 	}}
 }

From b0904c2e9974de684e0678f6baa4ce31c388468e Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Wed, 20 Mar 2024 11:39:55 -0400
Subject: [PATCH 13/17] change TestNewWebhookAuthenticator to test table style

---
 .../webhookcachefiller_test.go                | 188 +++++++++++-------
 1 file changed, 112 insertions(+), 76 deletions(-)

diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index 4d4b4c2a3..c92860390 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -1189,106 +1189,142 @@ func TestController(t *testing.T) {
 func TestNewWebhookAuthenticator(t *testing.T) {
 	goodEndpoint := "https://example.com"
 
-	t.Run("prerequisites not ready, cannot create webhook authenticator", func(t *testing.T) {
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator("", []byte("some pem bytes"), os.CreateTemp, clientcmd.WriteToFile, conditions, false)
-		require.Equal(t, []*metav1.Condition{
-			{
+	testServerCABundle, testServerURL := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
+		body, err := io.ReadAll(r.Body)
+		require.NoError(t, err)
+		require.Contains(t, string(body), "test-token")
+		_, err = w.Write([]byte(`{}`))
+		require.NoError(t, err)
+	})
+
+	tests := []struct {
+		name                            string
+		endpoint                        string
+		pemBytes                        []byte
+		tempFileFunc                    func(dir string, pattern string) (*os.File, error)
+		marshallFunc                    func(config clientcmdapi.Config, filename string) error
+		prereqOk                        bool
+		wantConditions                  []*metav1.Condition
+		wantWebhook                     bool
+		wantErr                         string
+		testCreatedWebhookWithFakeToken bool
+	}{
+		{
+			name:         "prerequisites not ready, cannot create webhook authenticator",
+			endpoint:     "",
+			pemBytes:     []byte("irrelevant pem bytes"),
+			tempFileFunc: os.CreateTemp,
+			marshallFunc: clientcmd.WriteToFile,
+			wantErr:      "",
+			wantConditions: []*metav1.Condition{{
 				Type:    "AuthenticatorValid",
 				Status:  "Unknown",
 				Reason:  "UnableToValidate",
 				Message: "unable to validate; see other conditions for details",
+			}},
+			prereqOk: false,
+		}, {
+			name:     "temp file failure, cannot create webhook authenticator",
+			endpoint: "",
+			pemBytes: []byte("irrelevant pem bytes"),
+			tempFileFunc: func(_ string, _ string) (*os.File, error) {
+				return nil, fmt.Errorf("some temp file error")
 			},
-		}, conditions)
-		require.Nil(t, res)
-		require.NoError(t, err)
-	})
-
-	t.Run("temp file failure, cannot create webhook authenticator", func(t *testing.T) {
-		brokenTempFile := func(_ string, _ string) (*os.File, error) { return nil, fmt.Errorf("some temp file error") }
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator("", []byte("some pem bytes"), brokenTempFile, clientcmd.WriteToFile, conditions, true)
-		require.Equal(t, []*metav1.Condition{
-			{
+			marshallFunc: clientcmd.WriteToFile,
+			prereqOk:     true,
+			wantConditions: []*metav1.Condition{{
 				Type:    "AuthenticatorValid",
 				Status:  "False",
 				Reason:  "UnableToCreateTempFile",
 				Message: "unable to create temporary file: some temp file error",
+			}},
+			wantErr: "unable to create temporary file: some temp file error",
+		}, {
+			name:         "marshal failure, cannot create webhook authenticator",
+			endpoint:     "",
+			pemBytes:     []byte("irrelevant pem bytes"),
+			tempFileFunc: os.CreateTemp,
+			marshallFunc: func(_ clientcmdapi.Config, _ string) error {
+				return fmt.Errorf("some marshal error")
 			},
-		}, conditions)
-		require.Nil(t, res)
-		require.EqualError(t, err, "unable to create temporary file: some temp file error")
-	})
-
-	t.Run("marshal failure, cannot create webhook authenticator", func(t *testing.T) {
-		marshalError := func(_ clientcmdapi.Config, _ string) error { return fmt.Errorf("some marshal error") }
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator("", []byte("some pem bytes"), os.CreateTemp, marshalError, conditions, true)
-		require.Equal(t, []*metav1.Condition{
-			{
+			prereqOk: true,
+			wantConditions: []*metav1.Condition{{
 				Type:    "AuthenticatorValid",
 				Status:  "False",
 				Reason:  "UnableToMarshallKubeconfig",
 				Message: "unable to marshal kubeconfig: some marshal error",
-			},
-		}, conditions)
-		require.Nil(t, res)
-		require.EqualError(t, err, "unable to marshal kubeconfig: some marshal error")
-	})
-
-	t.Run("invalid pem data, unable to parse bytes as PEM block", func(t *testing.T) {
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(goodEndpoint, []byte("invalid-bas64"), os.CreateTemp, clientcmd.WriteToFile, conditions, true)
-		require.Equal(t, []*metav1.Condition{
-			{
+			}},
+			wantErr: "unable to marshal kubeconfig: some marshal error",
+		}, {
+			name:         "invalid pem data, unable to parse bytes as PEM block",
+			endpoint:     goodEndpoint,
+			pemBytes:     []byte("invalid-bas64"),
+			tempFileFunc: os.CreateTemp,
+			marshallFunc: clientcmd.WriteToFile,
+			prereqOk:     true,
+			wantConditions: []*metav1.Condition{{
 				Type:    "AuthenticatorValid",
 				Status:  "False",
 				Reason:  "UnableToInstantiateWebhook",
 				Message: "unable to instantiate webhook: unable to load root certificates: unable to parse bytes as PEM block",
-			},
-		}, conditions)
-		require.Nil(t, res)
-		require.EqualError(t, err, "unable to instantiate webhook: unable to load root certificates: unable to parse bytes as PEM block")
-	})
-
-	t.Run("valid config with no TLS spec, webhook authenticator created", func(t *testing.T) {
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(goodEndpoint, nil, os.CreateTemp, clientcmd.WriteToFile, conditions, true)
-		require.Equal(t, []*metav1.Condition{
-			{
+			}},
+			wantErr: "unable to instantiate webhook: unable to load root certificates: unable to parse bytes as PEM block",
+		}, {
+			name:         "valid config with no TLS spec, webhook authenticator created",
+			endpoint:     goodEndpoint,
+			pemBytes:     nil,
+			tempFileFunc: os.CreateTemp,
+			marshallFunc: clientcmd.WriteToFile,
+			prereqOk:     true,
+			wantConditions: []*metav1.Condition{{
 				Type:    "AuthenticatorValid",
 				Status:  "True",
 				Reason:  "Success",
 				Message: "authenticator initialized",
-			},
-		}, conditions)
-		require.NotNil(t, res)
-		require.NoError(t, err)
-	})
-
-	t.Run("success, webhook authenticator created", func(t *testing.T) {
-		caBundle, url := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
-			body, err := io.ReadAll(r.Body)
-			require.NoError(t, err)
-			require.Contains(t, string(body), "test-token")
-			_, err = w.Write([]byte(`{}`))
-			require.NoError(t, err)
-		})
-		conditions := []*metav1.Condition{}
-		res, conditions, err := newWebhookAuthenticator(url, []byte(caBundle), os.CreateTemp, clientcmd.WriteToFile, conditions, true)
-		require.NoError(t, err)
-		require.NotNil(t, res)
-		require.Equal(t, []*metav1.Condition{
-			{
+			}},
+			wantWebhook: true,
+		}, {
+			name:         "success, webhook authenticator created",
+			endpoint:     testServerURL,
+			pemBytes:     []byte(testServerCABundle),
+			tempFileFunc: os.CreateTemp,
+			marshallFunc: clientcmd.WriteToFile,
+			prereqOk:     true,
+			wantConditions: []*metav1.Condition{{
 				Type:    "AuthenticatorValid",
 				Status:  "True",
 				Reason:  "Success",
 				Message: "authenticator initialized",
-			},
-		}, conditions)
-		resp, authenticated, err := res.AuthenticateToken(context.Background(), "test-token")
-		require.NoError(t, err)
-		require.Nil(t, resp)
-		require.False(t, authenticated)
-	})
+			}},
+			testCreatedWebhookWithFakeToken: true,
+		},
+	}
+
+	for _, tt := range tests {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			var conditions []*metav1.Condition
+			webhook, conditions, err := newWebhookAuthenticator(tt.endpoint, tt.pemBytes, tt.tempFileFunc, tt.marshallFunc, conditions, tt.prereqOk)
+
+			require.Equal(t, tt.wantConditions, conditions)
+
+			if tt.wantWebhook {
+				require.NotNil(t, webhook)
+			}
+
+			if tt.wantErr != "" {
+				require.EqualError(t, err, tt.wantErr)
+			} else {
+				require.NoError(t, err)
+			}
+
+			if tt.testCreatedWebhookWithFakeToken {
+				authResp, isAuthenticated, err := webhook.AuthenticateToken(context.Background(), "test-token")
+				require.NoError(t, err)
+				require.Nil(t, authResp)
+				require.False(t, isAuthenticated)
+			}
+		})
+	}
 }

From e38a27d93d8629a79ad1b50d172a599e4e40a53d Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Fri, 22 Mar 2024 11:57:05 -0400
Subject: [PATCH 14/17] Add endpointaddr.ParseFromURL helper,
 WebhookAuthenticator handle additional IPv6 cases

---
 .../webhookcachefiller/webhookcachefiller.go  |  43 +--
 .../webhookcachefiller_test.go                | 278 +++++++++++++++---
 internal/endpointaddr/endpointaddr.go         |  39 ++-
 internal/endpointaddr/endpointaddr_test.go    | 171 ++++++++++-
 4 files changed, 465 insertions(+), 66 deletions(-)

diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index b96c0c754..799128487 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -113,9 +113,9 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 	var errs []error
 
 	certPool, pemBytes, conditions, tlsBundleOk := c.validateTLSBundle(specCopy.TLS, conditions)
-	endpointURL, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
+	endpointHostPort, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
 	okSoFar := tlsBundleOk && endpointOk
-	conditions, tlsNegotiateErr := c.validateTLSNegotiation(certPool, endpointURL, conditions, okSoFar)
+	conditions, tlsNegotiateErr := c.validateConnectionProbe(certPool, endpointHostPort, conditions, okSoFar)
 	errs = append(errs, tlsNegotiateErr)
 	okSoFar = okSoFar && tlsNegotiateErr == nil
 
@@ -271,7 +271,7 @@ func newWebhookAuthenticator(
 	return webhookA, conditions, nil
 }
 
-func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.CertPool, endpointURL *url.URL, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
+func (c *webhookCacheFillerController) validateConnectionProbe(certPool *x509.CertPool, endpointHostPort *endpointaddr.HostPort, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
 	if !prereqOk {
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeConnectionProbeValid,
@@ -282,30 +282,22 @@ func (c *webhookCacheFillerController) validateTLSNegotiation(certPool *x509.Cer
 		return conditions, nil
 	}
 
-	// dial requires domain, IPv4 or IPv6 w/o protocol
-	endpointHostPort, err := endpointaddr.Parse(endpointURL.Host, 443)
-	if err != nil {
-		// we have already validated the endpoint with url.Parse(endpoint) in c.validateEndpoint()
-		// so there is no reason to have a parsing error here.
-		c.log.Error("error parsing endpoint", err)
-	}
-
-	conn, dialErr := c.tlsDialerFunc("tcp", endpointHostPort.Endpoint(), &tls.Config{
+	conn, err := c.tlsDialerFunc("tcp", endpointHostPort.Endpoint(), &tls.Config{
 		MinVersion: tls.VersionTLS12,
 		// If certPool is nil then RootCAs will be set to nil and TLS will use the host's root CA set automatically.
 		RootCAs: certPool,
 	})
 
-	if dialErr != nil {
+	if err != nil {
 		errText := "cannot dial server"
-		msg := fmt.Sprintf("%s: %s", errText, dialErr.Error())
+		msg := fmt.Sprintf("%s: %s", errText, err.Error())
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeConnectionProbeValid,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonUnableToDialServer,
 			Message: msg,
 		})
-		return conditions, fmt.Errorf("%s: %w", errText, dialErr)
+		return conditions, fmt.Errorf("%s: %w", errText, err)
 	}
 
 	// this error should never be significant
@@ -348,10 +340,10 @@ func (c *webhookCacheFillerController) validateTLSBundle(tlsSpec *auth1alpha1.TL
 	return rootCAs, pemBytes, conditions, true
 }
 
-func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditions []*metav1.Condition) (*url.URL, []*metav1.Condition, bool) {
+func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditions []*metav1.Condition) (*endpointaddr.HostPort, []*metav1.Condition, bool) {
 	endpointURL, err := url.Parse(endpoint)
 	if err != nil {
-		msg := fmt.Sprintf("%s: %s", "spec.endpoint URL is invalid", err.Error())
+		msg := fmt.Sprintf("%s: %s", "spec.endpoint URL cannot be parsed", err.Error())
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeEndpointURLValid,
 			Status:  metav1.ConditionFalse,
@@ -360,9 +352,10 @@ func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditi
 		})
 		return nil, conditions, false
 	}
+
 	// handles empty string and other issues as well.
 	if endpointURL.Scheme != "https" {
-		msg := fmt.Sprintf("spec.endpoint %s has invalid scheme, require 'https'", endpoint)
+		msg := fmt.Sprintf("spec.endpoint URL %s has invalid scheme, require 'https'", endpoint)
 		conditions = append(conditions, &metav1.Condition{
 			Type:    typeEndpointURLValid,
 			Status:  metav1.ConditionFalse,
@@ -372,13 +365,25 @@ func (c *webhookCacheFillerController) validateEndpoint(endpoint string, conditi
 		return nil, conditions, false
 	}
 
+	endpointHostPort, err := endpointaddr.ParseFromURL(endpointURL, 443)
+	if err != nil {
+		msg := fmt.Sprintf("%s: %s", "spec.endpoint URL is not valid", err.Error())
+		conditions = append(conditions, &metav1.Condition{
+			Type:    typeEndpointURLValid,
+			Status:  metav1.ConditionFalse,
+			Reason:  reasonInvalidEndpointURL,
+			Message: msg,
+		})
+		return nil, conditions, false
+	}
+
 	conditions = append(conditions, &metav1.Condition{
 		Type:    typeEndpointURLValid,
 		Status:  metav1.ConditionTrue,
 		Reason:  reasonSuccess,
 		Message: "endpoint is a valid URL",
 	})
-	return endpointURL, conditions, true
+	return &endpointHostPort, conditions, true
 }
 
 func (c *webhookCacheFillerController) updateStatus(
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index c92860390..657472bdb 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -149,9 +149,6 @@ func TestController(t *testing.T) {
 	localhostURL, err := url.Parse(hostAsLocalhostWebhookServer.URL)
 	require.NoError(t, err)
 
-	hostAs127001WebhookServerURL, err := url.Parse(hostAs127001WebhookServer.URL)
-	require.NoError(t, err)
-
 	badEndpointInvalidURL := "https://.café   .com/café/café/café/coffee"
 	badEndpointNoHTTPS := "http://localhost"
 
@@ -307,6 +304,16 @@ func TestController(t *testing.T) {
 			Message:            "cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs",
 		}
 	}
+	sadConnectionProbeValidWithMessage := func(time metav1.Time, observedGeneration int64, msg string) metav1.Condition {
+		return metav1.Condition{
+			Type:               "ConnectionProbeValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "UnableToDialServer",
+			Message:            msg,
+		}
+	}
 
 	happyEndpointURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
@@ -325,17 +332,28 @@ func TestController(t *testing.T) {
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
 			Reason:             "InvalidEndpointURL",
-			Message:            fmt.Sprintf(`spec.endpoint URL is invalid: parse "%s": invalid character " " in host name`, issuer),
+			Message:            fmt.Sprintf(`spec.endpoint URL cannot be parsed: parse "%s": invalid character " " in host name`, issuer),
 		}
 	}
-	sadEndpointURLValidHTTPS := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadEndpointURLValidHTTPS := func(endpoint string, time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
 			Type:               "EndpointURLValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
 			Reason:             "InvalidEndpointURLScheme",
-			Message:            fmt.Sprintf(`spec.endpoint %s has invalid scheme, require 'https'`, issuer),
+			Message:            fmt.Sprintf(`spec.endpoint URL %s has invalid scheme, require 'https'`, endpoint),
+		}
+	}
+
+	sadEndpointURLValidWithMessage := func(time metav1.Time, observedGeneration int64, msg string) metav1.Condition {
+		return metav1.Condition{
+			Type:               "EndpointURLValid",
+			Status:             "False",
+			ObservedGeneration: observedGeneration,
+			LastTransitionTime: time,
+			Reason:             "InvalidEndpointURL",
+			Message:            msg,
 		}
 	}
 
@@ -425,7 +443,8 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
+		},
+		{
 			name:    "Sync: changed WebhookAuthenticator: loop will update timestamps only on relevant statuses",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
@@ -482,7 +501,8 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
+		},
+		{
 			name:    "Sync: valid WebhookAuthenticator with CA: will complete sync loop successfully with success conditions and ready phase",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
@@ -647,7 +667,8 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 0,
-		}, {
+		},
+		{
 			name:    "validateEndpoint: parsing error (spec.endpoint URL has invalid scheme, requires https) will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
@@ -692,7 +713,59 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 0,
 		},
 		{
-			name:    "validateTLSNegotiation: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
+			name:    "validateEndpoint: should error if endpoint cannot be parsed",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
+				return nil, errors.New("IPv6 test fake error")
+			},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: "https://[0:0:0:0:0:0:0:1]:69999/some/fake/path",
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: "https://[0:0:0:0:0:0:0:1]:69999/some/fake/path",
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess("https://[0:0:0:0:0:0:0:1]:69999/some/fake/path", frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadEndpointURLValidWithMessage(frozenMetav1Now, 0, `spec.endpoint URL is not valid: invalid port "69999"`),
+								sadReadyCondition(frozenMetav1Now, 0),
+								unknownConnectionProbeValid(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantCacheEntries: 0,
+		},
+		{
+			name:    "validateConnectionProbe: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -732,9 +805,9 @@ func TestController(t *testing.T) {
 		},
 		// No unit test for system roots.  We don't test the JWTAuthenticator's use of system roots either.
 		// We would have to find a way to mock out roots by adding a dummy cert in order to test this
-		// { name: "validateTLSNegotiation: TLS bundle not provided should use system roots to validate server cert signed by a well-known CA",},
+		// { name: "validateConnectionProbe: TLS bundle not provided should use system roots to validate server cert signed by a well-known CA",},
 		{
-			name:    "validateTLSNegotiation: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase",
+			name:    "validateConnectionProbe: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -775,8 +848,9 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
-			name:    "validateTLSNegotiation: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4",
+		},
+		{
+			name:    "validateConnectionProbe: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -815,16 +889,17 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
-			name:    "validateTLSNegotiation: localhost IPv6 address instead of 127.0.0.1 should still dial correctly as dialer should handle addresses",
+		},
+		{
+			name:    "validateConnectionProbe: IPv6 address with port: should call dialer func with correct arguments",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
-				// We are dialing a different server here since CI doesn't have the linux IPv6 stack installed.
-				// This test proves that IPv6 addresses are parsed & handled correctly before we call tls.Dial in production code.
-				return tls.Dial(network, hostAs127001WebhookServerURL.Host, &tls.Config{
-					MinVersion: tls.VersionTLS12,
-					RootCAs:    caForLocalhostAs127001.Pool(),
-				})
+				assert.Equal(t, "tcp", network)
+				assert.Equal(t, "[0:0:0:0:0:0:0:1]:4242", addr)
+				assert.True(t, caForLocalhostAs127001.Pool().Equal(config.RootCAs))
+				assert.Equal(t, uint16(tls.VersionTLS12), config.MinVersion)
+
+				return nil, errors.New("IPv6 test fake error to skip real dial in prod code, this is actually success")
 			},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -832,22 +907,67 @@ func TestController(t *testing.T) {
 						Name: "test-name",
 					},
 					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()),
+						Endpoint: "https://[0:0:0:0:0:0:0:1]:4242/some/fake/path",
 						TLS: &auth1alpha1.TLSSpec{
 							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
 						},
 					},
 				},
 			},
-			wantLogs: []map[string]any{
-				{
-					"level":     "info",
-					"timestamp": "2099-08-08T13:57:36.123456Z",
-					"logger":    "webhookcachefiller-controller",
-					"message":   "added new webhook authenticator",
-					"endpoint":  fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()),
-					"webhook": map[string]interface{}{
-						"name": "test-name",
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: "https://[0:0:0:0:0:0:0:1]:4242/some/fake/path",
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess("https://[0:0:0:0:0:0:0:1]:4242/some/fake/path", frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadConnectionProbeValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
+								sadReadyCondition(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success`),
+			wantCacheEntries: 0,
+		},
+		{
+			name:    "validateConnectionProbe: IPv6 address without port: should call dialer func with correct arguments",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
+				assert.Equal(t, "tcp", network)
+				assert.Equal(t, "[0:0:0:0:0:0:0:1]:443", addr, "should add default port when port not provided")
+				assert.True(t, caForLocalhostAs127001.Pool().Equal(config.RootCAs))
+				assert.Equal(t, uint16(tls.VersionTLS12), config.MinVersion)
+
+				return nil, errors.New("IPv6 test fake error to skip real dial in prod code, this is actually success")
+			},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: "https://[0:0:0:0:0:0:0:1]/some/fake/path",
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
 					},
 				},
 			},
@@ -857,14 +977,21 @@ func TestController(t *testing.T) {
 						Name: "test-name",
 					},
 					Spec: auth1alpha1.WebhookAuthenticatorSpec{
-						Endpoint: fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()),
+						Endpoint: "https://[0:0:0:0:0:0:0:1]/some/fake/path",
 						TLS: &auth1alpha1.TLSSpec{
 							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
 						},
 					},
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
-						Conditions: allHappyConditionsSuccess(fmt.Sprintf("%s:%s", "https://[0:0:0:0:0:0:0:1]", hostAs127001WebhookServerURL.Port()), frozenMetav1Now, 0),
-						Phase:      "Ready",
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess("https://[0:0:0:0:0:0:0:1]/some/fake/path", frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadConnectionProbeValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
+								sadReadyCondition(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
 					},
 				})
 				updateStatusAction.Subresource = "status"
@@ -874,9 +1001,11 @@ func TestController(t *testing.T) {
 					updateStatusAction,
 				}
 			},
-			wantCacheEntries: 1,
-		}, {
-			name:    "validateTLSNegotiation: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
+			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success`),
+			wantCacheEntries: 0,
+		},
+		{
+			name:    "validateConnectionProbe: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -914,8 +1043,9 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
-			name:    "validateTLSNegotiation: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
+		},
+		{
+			name:    "validateConnectionProbe: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -956,7 +1086,65 @@ func TestController(t *testing.T) {
 			},
 			wantCacheEntries: 0,
 			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`),
-		}, {
+		},
+		{
+			name:    "validateConnectionProbe: IPv6 address without port or brackets: should succeed since IPv6 brackets are optional without port",
+			syncKey: controllerlib.Key{Name: "test-name"},
+			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
+				assert.Equal(t, "tcp", network)
+				assert.Equal(t, "[0:0:0:0:0:0:0:1]:443", addr)
+				assert.True(t, caForLocalhostAs127001.Pool().Equal(config.RootCAs))
+				assert.Equal(t, uint16(tls.VersionTLS12), config.MinVersion)
+
+				return nil, errors.New("IPv6 test fake error to skip real dial in prod code, this is actually success")
+			},
+			webhooks: []runtime.Object{
+				&auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: "https://0:0:0:0:0:0:0:1/some/fake/path",
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+				},
+			},
+			wantActions: func() []coretesting.Action {
+				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
+					ObjectMeta: metav1.ObjectMeta{
+						Name: "test-name",
+					},
+					Spec: auth1alpha1.WebhookAuthenticatorSpec{
+						Endpoint: "https://0:0:0:0:0:0:0:1/some/fake/path",
+						TLS: &auth1alpha1.TLSSpec{
+							CertificateAuthorityData: base64.StdEncoding.EncodeToString(caForLocalhostAs127001.Bundle()),
+						},
+					},
+					Status: auth1alpha1.WebhookAuthenticatorStatus{
+						Conditions: conditionstestutil.Replace(
+							allHappyConditionsSuccess("https://0:0:0:0:0:0:0:1/some/fake/path", frozenMetav1Now, 0),
+							[]metav1.Condition{
+								sadConnectionProbeValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
+								sadReadyCondition(frozenMetav1Now, 0),
+								unknownAuthenticatorValid(frozenMetav1Now, 0),
+							},
+						),
+						Phase: "Error",
+					},
+				})
+				updateStatusAction.Subresource = "status"
+				return []coretesting.Action{
+					coretesting.NewListAction(webhookAuthenticatorGVR, webhookAuthenticatorGVK, "", metav1.ListOptions{}),
+					coretesting.NewWatchAction(webhookAuthenticatorGVR, "", metav1.ListOptions{}),
+					updateStatusAction,
+				}
+			},
+			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success`),
+			wantCacheEntries: 0,
+		},
+		{
 			name:    "updateStatus: called with matching original and updated conditions: will not make request to update conditions",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
@@ -990,7 +1178,8 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
+		},
+		{
 			name:    "updateStatus: called with different original and updated conditions: will make request to update conditions",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
@@ -1041,7 +1230,8 @@ func TestController(t *testing.T) {
 				}
 			},
 			wantCacheEntries: 1,
-		}, {
+		},
+		{
 			name:    "updateStatus: when update request fails: error will enqueue a resync",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			configClient: func(client *pinnipedfake.Clientset) {
@@ -1149,7 +1339,7 @@ func TestController(t *testing.T) {
 				require.NoError(t, err)
 			}
 			actualLogLines := stringutil.SplitByNewline(log.String())
-			require.Equal(t, len(actualLogLines), len(tt.wantLogs), "log line count should be correct")
+			require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct")
 
 			for logLineNum, logLine := range actualLogLines {
 				require.NotNil(t, tt.wantLogs[logLineNum], "expected log line should never be empty")
diff --git a/internal/endpointaddr/endpointaddr.go b/internal/endpointaddr/endpointaddr.go
index fd9277658..75c248b4a 100644
--- a/internal/endpointaddr/endpointaddr.go
+++ b/internal/endpointaddr/endpointaddr.go
@@ -1,4 +1,4 @@
-// Copyright 2021 the Pinniped contributors. All Rights Reserved.
+// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 // Package endpointaddr implements parsing and validation of "<host>[:<port>]" strings for Pinniped APIs.
@@ -7,7 +7,9 @@ package endpointaddr
 import (
 	"fmt"
 	"net"
+	"net/url"
 	"strconv"
+	"strings"
 
 	"k8s.io/apimachinery/pkg/util/validation"
 )
@@ -38,7 +40,7 @@ func (h *HostPort) Endpoint() string {
 // - "<IPv4>:<port>"     (IPv4 address with port)
 // - "[<IPv6>]:<port>"   (IPv6 address with port, brackets are required)
 //
-// If the input does not not specify a port number, then defaultPort will be used.
+// If the input does not specify a port number, then defaultPort will be used.
 func Parse(endpoint string, defaultPort uint16) (HostPort, error) {
 	// Try parsing it both with and without an implicit port 443 at the end.
 	host, port, err := net.SplitHostPort(endpoint)
@@ -69,3 +71,36 @@ func Parse(endpoint string, defaultPort uint16) (HostPort, error) {
 
 	return HostPort{Host: host, Port: uint16(integerPort)}, nil
 }
+
+// ParseFromURL wraps Parse but specifically takes a url.URL instead of an endpoint string.
+// ParseFromURL differs from Parse in that a URL will contain a protocol, and IPv6 addresses
+// may or may not be wrapped in brackets (but require them when a port is provided):
+//
+// - "https://<hostname>"        (DNS hostname)
+// - "https://<IPv4>"            (IPv4 address)
+// - "https://<IPv6>"            (IPv6 address)
+// - "https://[<IPv6>]"          (IPv6 address without port, brackets should be used but are not strictly required)
+// - "https://<hostname>:<port>" (DNS hostname with port)
+// - "https://<IPv4>:<port>"     (IPv4 address with port)
+// - "https://[<IPv6>]:<port>"   (IPv6 address with port, brackets are required)
+//
+// If the input does not specify a port number, then defaultPort will be used.
+//
+// The rfc for literal IPv6 addresses in URLs indicates that brackets
+// - must be used when a port is provided
+// - should be used when a port is not provided, but does not indicate "must"
+//
+// Since url.Parse does not inspect the host, it will accept IPv6 hosts without
+// brackets and without port, which may result in errors that are not immediately obvious.
+// Therefore, this helper will normalize the bracketed use case.  Note that this is
+// because ParseFromURL returns a HostPort which has an Endpoint() method which will
+// return a properly constructed URL with brackets when appropriate.
+//
+// See RFC: https://datatracker.ietf.org/doc/html/rfc2732#section-2
+func ParseFromURL(u *url.URL, defaultPort uint16) (HostPort, error) {
+	host := u.Host
+	if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
+		host = strings.TrimPrefix(strings.TrimSuffix(host, "]"), "[")
+	}
+	return Parse(host, defaultPort)
+}
diff --git a/internal/endpointaddr/endpointaddr_test.go b/internal/endpointaddr/endpointaddr_test.go
index 736df3121..adf6bb0b7 100644
--- a/internal/endpointaddr/endpointaddr_test.go
+++ b/internal/endpointaddr/endpointaddr_test.go
@@ -1,12 +1,14 @@
-// Copyright 2021 the Pinniped contributors. All Rights Reserved.
+// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
 // SPDX-License-Identifier: Apache-2.0
 
 package endpointaddr
 
 import (
+	"net/url"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 func TestParse(t *testing.T) {
@@ -180,3 +182,170 @@ func TestParse(t *testing.T) {
 		})
 	}
 }
+
+func TestParseFromURL(t *testing.T) {
+	t.Parallel()
+	for _, tt := range []struct {
+		name        string
+		input       string
+		defaultPort uint16
+		expectErr   string
+		expect      HostPort
+		// HostPort.Endpoint() returns a properly constructed endpoint.  The normalization provided by ParseFromURL()
+		// expects that the resulting HostPort.Endpoint() will be called to normalize several special cases, especially
+		// for IPv6.
+		expectEndpoint string
+	}{
+		// First set of valid passthrough tests to Parse()
+		// Matches the above test table, minus any test that would not url.Parse(input) properly
+		{
+			name:           "plain IPv4",
+			input:          "http://127.0.0.1",
+			defaultPort:    443,
+			expect:         HostPort{Host: "127.0.0.1", Port: 443},
+			expectEndpoint: "127.0.0.1:443",
+		},
+		{
+			name:           "IPv4 with port",
+			input:          "http://127.0.0.1:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "127.0.0.1", Port: 8443},
+			expectEndpoint: "127.0.0.1:8443",
+		},
+		{
+			name:           "IPv4 in brackets with port",
+			input:          "http://[127.0.0.1]:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "127.0.0.1", Port: 8443},
+			expectEndpoint: "127.0.0.1:8443",
+		},
+		{
+			name:           "IPv4 as IPv6 in brackets with port",
+			input:          "http://[::127.0.0.1]:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "::127.0.0.1", Port: 8443},
+			expectEndpoint: "[::127.0.0.1]:8443",
+		},
+		{
+			name:           "IPv6 with port",
+			input:          "http://[2001:db8::ffff]:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "2001:db8::ffff", Port: 8443},
+			expectEndpoint: "[2001:db8::ffff]:8443",
+		},
+		{
+			name:           "plain hostname",
+			input:          "http://host.example.com",
+			defaultPort:    443,
+			expect:         HostPort{Host: "host.example.com", Port: 443},
+			expectEndpoint: "host.example.com:443",
+		},
+		{
+			name:           "plain hostname with dash",
+			input:          "http://host-dev.example.com",
+			defaultPort:    443,
+			expect:         HostPort{Host: "host-dev.example.com", Port: 443},
+			expectEndpoint: "host-dev.example.com:443",
+		},
+		{
+			name:           "hostname with port",
+			input:          "http://host.example.com:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "host.example.com", Port: 8443},
+			expectEndpoint: "host.example.com:8443",
+		},
+		{
+			name:           "hostname in brackets with port",
+			input:          "http://[host.example.com]:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "host.example.com", Port: 8443},
+			expectEndpoint: "host.example.com:8443",
+		},
+		{
+			name:           "hostname without dots",
+			input:          "http://localhost",
+			defaultPort:    443,
+			expect:         HostPort{Host: "localhost", Port: 443},
+			expectEndpoint: "localhost:443",
+		},
+		{
+			name:           "hostname and port without dots",
+			input:          "http://localhost:8443",
+			defaultPort:    443,
+			expect:         HostPort{Host: "localhost", Port: 8443},
+			expectEndpoint: "localhost:8443",
+		},
+		{
+			name:        "http://invalid empty string",
+			input:       "",
+			defaultPort: 443,
+			expectErr:   `host "" is not a valid hostname or IP address`,
+		},
+		{
+			name:        "invalid host with underscores",
+			input:       "http://___.example.com:1234",
+			defaultPort: 443,
+			expectErr:   `host "___.example.com" is not a valid hostname or IP address`,
+		},
+		{
+			name:        "invalid host with uppercase",
+			input:       "http://HOST.EXAMPLE.COM",
+			defaultPort: 443,
+			expectErr:   `host "HOST.EXAMPLE.COM" is not a valid hostname or IP address`,
+		},
+		// new tests for new functionality
+		{
+			name:           "IPv6 with brackets but without port will strip brackets to create HostPort{}, which will add brackets when HostPort.Endpoint() is called",
+			input:          "http://[2001:db8::ffff]",
+			defaultPort:    443,
+			expect:         HostPort{Host: "2001:db8::ffff", Port: 443},
+			expectEndpoint: "[2001:db8::ffff]:443",
+		},
+		{
+			name:           "IPv6 without brackets and without port will create HostPort{}, which will add brackets when HostPort.Endpoint() is called",
+			input:          "http://2001:db8::1234",
+			defaultPort:    443,
+			expect:         HostPort{Host: "2001:db8::1234", Port: 443},
+			expectEndpoint: "[2001:db8::1234]:443",
+		},
+		{
+			name:           "IPv6 without brackets and without port with path create HostPort{}, which will add brackets when HostPort.Endpoint() is called",
+			input:          "https://0:0:0:0:0:0:0:1/some/fake/path",
+			defaultPort:    443,
+			expect:         HostPort{Host: "0:0:0:0:0:0:0:1", Port: 443},
+			expectEndpoint: "[0:0:0:0:0:0:0:1]:443",
+		},
+		{
+			name:           "IPv6 with mismatched leading bracket will err on bracket",
+			input:          "https://[[::1]/some/fake/path",
+			defaultPort:    443,
+			expect:         HostPort{Host: "[[::1]", Port: 443},
+			expectEndpoint: "[[::1]:443",
+			expectErr:      `address [[::1]:443: unexpected '[' in address`,
+		},
+		{
+			name:           "IPv6 with mismatched trailing brackets will err on port",
+			input:          "https://[::1]]/some/fake/path",
+			defaultPort:    443,
+			expect:         HostPort{Host: "[::1]]", Port: 443},
+			expectEndpoint: "[::1]]:443",
+			expectErr:      `address [::1]]:443: missing port in address`,
+		},
+	} {
+		tt := tt
+		t.Run(tt.name, func(t *testing.T) {
+			urlToProcess, err := url.Parse(tt.input)
+			require.NoError(t, err, "ParseFromURL expects a valid url.URL, parse errors here are not valuable")
+
+			got, err := ParseFromURL(urlToProcess, tt.defaultPort)
+			if tt.expectErr == "" {
+				assert.NoError(t, err)
+				assert.Equal(t, tt.expect, got)
+				assert.Equal(t, tt.expectEndpoint, got.Endpoint())
+			} else {
+				assert.EqualError(t, err, tt.expectErr)
+				assert.Equal(t, HostPort{}, got)
+			}
+		})
+	}
+}

From eed0c9d5b0e4898f6cff1593118d2ce1aca4c94c Mon Sep 17 00:00:00 2001
From: Ben Petersen <ben@benjaminapetersen.me>
Date: Tue, 26 Mar 2024 15:32:15 -0400
Subject: [PATCH 15/17] Update ParseFromURL usage comment.

- Carefully note the rfc27732 design for IPv6 in URLs, while also clarifying the handling of IPv6 in Golang.

Co-authored-by: Ryan Richard <richardry@vmware.com>
---
 internal/endpointaddr/endpointaddr.go | 26 +++++++++-----------------
 1 file changed, 9 insertions(+), 17 deletions(-)

diff --git a/internal/endpointaddr/endpointaddr.go b/internal/endpointaddr/endpointaddr.go
index 75c248b4a..c6d0223af 100644
--- a/internal/endpointaddr/endpointaddr.go
+++ b/internal/endpointaddr/endpointaddr.go
@@ -73,30 +73,22 @@ func Parse(endpoint string, defaultPort uint16) (HostPort, error) {
 }
 
 // ParseFromURL wraps Parse but specifically takes a url.URL instead of an endpoint string.
-// ParseFromURL differs from Parse in that a URL will contain a protocol, and IPv6 addresses
-// may or may not be wrapped in brackets (but require them when a port is provided):
-//
-// - "https://<hostname>"        (DNS hostname)
-// - "https://<IPv4>"            (IPv4 address)
-// - "https://<IPv6>"            (IPv6 address)
-// - "https://[<IPv6>]"          (IPv6 address without port, brackets should be used but are not strictly required)
-// - "https://<hostname>:<port>" (DNS hostname with port)
-// - "https://<IPv4>:<port>"     (IPv4 address with port)
-// - "https://[<IPv6>]:<port>"   (IPv6 address with port, brackets are required)
+// ParseFromURL differs from Parse in that IPv6 addresses must be wrapped in brackets
+// when used in a URL (even when used without a port).
 //
 // If the input does not specify a port number, then defaultPort will be used.
 //
-// The rfc for literal IPv6 addresses in URLs indicates that brackets
+// The RFC for literal IPv6 addresses in URLs indicates that brackets
 // - must be used when a port is provided
 // - should be used when a port is not provided, but does not indicate "must"
+// See https://datatracker.ietf.org/doc/html/rfc2732#section-2
 //
-// Since url.Parse does not inspect the host, it will accept IPv6 hosts without
-// brackets and without port, which may result in errors that are not immediately obvious.
-// Therefore, this helper will normalize the bracketed use case.  Note that this is
-// because ParseFromURL returns a HostPort which has an Endpoint() method which will
-// return a properly constructed URL with brackets when appropriate.
+// However, the Golang docs make it clear that IPv6 addresses must be wrapped
+// in brackets when used in a URL.
+// See https://pkg.go.dev/net/url#URL
 //
-// See RFC: https://datatracker.ietf.org/doc/html/rfc2732#section-2
+// Note that ParseFromURL returns a HostPort which has an Endpoint() method which
+// will return a properly constructed URL with brackets when appropriate.
 func ParseFromURL(u *url.URL, defaultPort uint16) (HostPort, error) {
 	host := u.Host
 	if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {

From f86c46e160fbfcd3c3e4857875d2b64a5a415274 Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Tue, 26 Mar 2024 15:33:44 -0400
Subject: [PATCH 16/17] Update WebhookAuthenticator Status
 WebhookConnectionValid

- ConnectionProbeValid -> WebhookConnectionValid
  - This is to conform with the pattern of other controllers, ex:
    LDAPConnectionValid
---
 .../jwtcachefiller/jwtcachefiller.go          |  3 -
 .../jwtcachefiller/jwtcachefiller_test.go     |  6 --
 .../webhookcachefiller/webhookcachefiller.go  | 15 ++---
 .../webhookcachefiller_test.go                | 65 +++++++++----------
 ...cierge_webhookauthenticator_status_test.go | 16 ++---
 5 files changed, 44 insertions(+), 61 deletions(-)

diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
index f543b2e6f..951db2bfa 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go
@@ -606,8 +606,5 @@ func (c *jwtCacheFillerController) updateStatus(
 		return nil
 	}
 	_, err := c.client.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
-	if err != nil {
-		c.log.Info(fmt.Sprintf("ERROR: %v", err))
-	}
 	return err
 }
diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index 1ceaa3fa1..8759f8437 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -1652,12 +1652,6 @@ func TestController(t *testing.T) {
 				"jwtAuthenticator": map[string]interface{}{
 					"name": "test-name",
 				},
-			}, {
-				"level":     "info",
-				"timestamp": "2099-08-08T13:57:36.123456Z",
-				"logger":    "jwtcachefiller-controller",
-				"message":   "ERROR: some update error",
-				"issuer":    goodIssuer,
 			}},
 			wantSyncLoopErr:  testutil.WantExactErrorString("some update error"),
 			wantCacheEntries: 1,
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
index 799128487..a6ebd96a9 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller.go
@@ -42,7 +42,7 @@ const (
 	controllerName                   = "webhookcachefiller-controller"
 	typeReady                        = "Ready"
 	typeTLSConfigurationValid        = "TLSConfigurationValid"
-	typeConnectionProbeValid         = "ConnectionProbeValid"
+	typeWebhookConnectionValid       = "WebhookConnectionValid"
 	typeEndpointURLValid             = "EndpointURLValid"
 	typeAuthenticatorValid           = "AuthenticatorValid"
 	reasonSuccess                    = "Success"
@@ -115,7 +115,7 @@ func (c *webhookCacheFillerController) Sync(ctx controllerlib.Context) error {
 	certPool, pemBytes, conditions, tlsBundleOk := c.validateTLSBundle(specCopy.TLS, conditions)
 	endpointHostPort, conditions, endpointOk := c.validateEndpoint(specCopy.Endpoint, conditions)
 	okSoFar := tlsBundleOk && endpointOk
-	conditions, tlsNegotiateErr := c.validateConnectionProbe(certPool, endpointHostPort, conditions, okSoFar)
+	conditions, tlsNegotiateErr := c.validateConnection(certPool, endpointHostPort, conditions, okSoFar)
 	errs = append(errs, tlsNegotiateErr)
 	okSoFar = okSoFar && tlsNegotiateErr == nil
 
@@ -271,10 +271,10 @@ func newWebhookAuthenticator(
 	return webhookA, conditions, nil
 }
 
-func (c *webhookCacheFillerController) validateConnectionProbe(certPool *x509.CertPool, endpointHostPort *endpointaddr.HostPort, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
+func (c *webhookCacheFillerController) validateConnection(certPool *x509.CertPool, endpointHostPort *endpointaddr.HostPort, conditions []*metav1.Condition, prereqOk bool) ([]*metav1.Condition, error) {
 	if !prereqOk {
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeConnectionProbeValid,
+			Type:    typeWebhookConnectionValid,
 			Status:  metav1.ConditionUnknown,
 			Reason:  reasonUnableToValidate,
 			Message: msgUnableToValidate,
@@ -292,7 +292,7 @@ func (c *webhookCacheFillerController) validateConnectionProbe(certPool *x509.Ce
 		errText := "cannot dial server"
 		msg := fmt.Sprintf("%s: %s", errText, err.Error())
 		conditions = append(conditions, &metav1.Condition{
-			Type:    typeConnectionProbeValid,
+			Type:    typeWebhookConnectionValid,
 			Status:  metav1.ConditionFalse,
 			Reason:  reasonUnableToDialServer,
 			Message: msg,
@@ -307,7 +307,7 @@ func (c *webhookCacheFillerController) validateConnectionProbe(certPool *x509.Ce
 	}
 
 	conditions = append(conditions, &metav1.Condition{
-		Type:    typeConnectionProbeValid,
+		Type:    typeWebhookConnectionValid,
 		Status:  metav1.ConditionTrue,
 		Reason:  reasonSuccess,
 		Message: "tls verified",
@@ -424,8 +424,5 @@ func (c *webhookCacheFillerController) updateStatus(
 	}
 
 	_, err := c.client.AuthenticationV1alpha1().WebhookAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
-	if err != nil {
-		c.log.Info(fmt.Sprintf("ERROR: %v", err))
-	}
 	return err
 }
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index 657472bdb..cecd3e47e 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -264,9 +264,9 @@ func TestController(t *testing.T) {
 		}
 	}
 
-	happyConnectionProbeValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	happyWebhookConnectionValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "ConnectionProbeValid",
+			Type:               "WebhookConnectionValid",
 			Status:             "True",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -274,9 +274,9 @@ func TestController(t *testing.T) {
 			Message:            "tls verified",
 		}
 	}
-	unknownConnectionProbeValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	unknownWebhookConnectionValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "ConnectionProbeValid",
+			Type:               "WebhookConnectionValid",
 			Status:             "Unknown",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -284,9 +284,9 @@ func TestController(t *testing.T) {
 			Message:            "unable to validate; see other conditions for details",
 		}
 	}
-	sadConnectionProbeValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadWebhookConnectionValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "ConnectionProbeValid",
+			Type:               "WebhookConnectionValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -294,9 +294,9 @@ func TestController(t *testing.T) {
 			Message:            "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
 		}
 	}
-	sadConnectionProbeValidNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
+	sadWebhookConnectionValidNoIPSANs := func(time metav1.Time, observedGeneration int64) metav1.Condition {
 		return metav1.Condition{
-			Type:               "ConnectionProbeValid",
+			Type:               "WebhookConnectionValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -304,9 +304,9 @@ func TestController(t *testing.T) {
 			Message:            "cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs",
 		}
 	}
-	sadConnectionProbeValidWithMessage := func(time metav1.Time, observedGeneration int64, msg string) metav1.Condition {
+	sadWebhookConnectionValidWithMessage := func(time metav1.Time, observedGeneration int64, msg string) metav1.Condition {
 		return metav1.Condition{
-			Type:               "ConnectionProbeValid",
+			Type:               "WebhookConnectionValid",
 			Status:             "False",
 			ObservedGeneration: observedGeneration,
 			LastTransitionTime: time,
@@ -361,7 +361,7 @@ func TestController(t *testing.T) {
 		return conditionstestutil.SortByType([]metav1.Condition{
 			happyTLSConfigurationValidCAParsed(someTime, observedGeneration),
 			happyEndpointURLValid(someTime, observedGeneration),
-			happyConnectionProbeValid(someTime, observedGeneration),
+			happyWebhookConnectionValid(someTime, observedGeneration),
 			happyAuthenticatorValid(someTime, observedGeneration),
 			happyReadyCondition(someTime, observedGeneration),
 		})
@@ -567,7 +567,7 @@ func TestController(t *testing.T) {
 							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
-								sadConnectionProbeValid(frozenMetav1Now, 0),
+								sadWebhookConnectionValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
@@ -607,7 +607,7 @@ func TestController(t *testing.T) {
 							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
 							[]metav1.Condition{
 								sadTLSConfigurationValid(frozenMetav1Now, 0),
-								unknownConnectionProbeValid(frozenMetav1Now, 0),
+								unknownWebhookConnectionValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -651,7 +651,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValid("https://.café   .com/café/café/café/coffee", frozenMetav1Now, 0),
-								unknownConnectionProbeValid(frozenMetav1Now, 0),
+								unknownWebhookConnectionValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -695,7 +695,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
 								sadEndpointURLValidHTTPS("http://localhost", frozenMetav1Now, 0),
-								unknownConnectionProbeValid(frozenMetav1Now, 0),
+								unknownWebhookConnectionValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -748,7 +748,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								sadEndpointURLValidWithMessage(frozenMetav1Now, 0, `spec.endpoint URL is not valid: invalid port "69999"`),
 								sadReadyCondition(frozenMetav1Now, 0),
-								unknownConnectionProbeValid(frozenMetav1Now, 0),
+								unknownWebhookConnectionValid(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
 						),
@@ -765,7 +765,7 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 0,
 		},
 		{
-			name:    "validateConnectionProbe: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
+			name:    "validateConnection: CA does not validate serving certificate for host, the dialer will error, will fail sync loop, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -788,7 +788,7 @@ func TestController(t *testing.T) {
 							[]metav1.Condition{
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
-								sadConnectionProbeValid(frozenMetav1Now, 0),
+								sadWebhookConnectionValid(frozenMetav1Now, 0),
 							},
 						),
 						Phase: "Error",
@@ -805,9 +805,9 @@ func TestController(t *testing.T) {
 		},
 		// No unit test for system roots.  We don't test the JWTAuthenticator's use of system roots either.
 		// We would have to find a way to mock out roots by adding a dummy cert in order to test this
-		// { name: "validateConnectionProbe: TLS bundle not provided should use system roots to validate server cert signed by a well-known CA",},
+		// { name: "validateConnection: TLS bundle not provided should use system roots to validate server cert signed by a well-known CA",},
 		{
-			name:    "validateConnectionProbe: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase",
+			name:    "validateConnection: 404 endpoint on a valid server will still validate server certificate, will complete sync loop successfully with success conditions and ready phase",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -850,7 +850,7 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 1,
 		},
 		{
-			name:    "validateConnectionProbe: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4",
+			name:    "validateConnection: localhost hostname instead of 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -891,7 +891,7 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 1,
 		},
 		{
-			name:    "validateConnectionProbe: IPv6 address with port: should call dialer func with correct arguments",
+			name:    "validateConnection: IPv6 address with port: should call dialer func with correct arguments",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
 				assert.Equal(t, "tcp", network)
@@ -929,7 +929,7 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess("https://[0:0:0:0:0:0:0:1]:4242/some/fake/path", frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadConnectionProbeValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
+								sadWebhookConnectionValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
 								sadReadyCondition(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
@@ -948,7 +948,7 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 0,
 		},
 		{
-			name:    "validateConnectionProbe: IPv6 address without port: should call dialer func with correct arguments",
+			name:    "validateConnection: IPv6 address without port: should call dialer func with correct arguments",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
 				assert.Equal(t, "tcp", network)
@@ -986,7 +986,7 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess("https://[0:0:0:0:0:0:0:1]/some/fake/path", frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadConnectionProbeValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
+								sadWebhookConnectionValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
 								sadReadyCondition(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
@@ -1005,7 +1005,7 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 0,
 		},
 		{
-			name:    "validateConnectionProbe: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
+			name:    "validateConnection: localhost as IP address 127.0.0.1 should still dial correctly as dialer should handle hostnames as well as IPv4 and IPv6 addresses",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -1045,7 +1045,7 @@ func TestController(t *testing.T) {
 			wantCacheEntries: 1,
 		},
 		{
-			name:    "validateConnectionProbe: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
+			name:    "validateConnection: CA for example.com, serving cert for example.com, but endpoint 127.0.0.1 will fail to validate certificate and will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
@@ -1069,7 +1069,7 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess(hostLocalWithExampleDotComCertServer.URL, frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadConnectionProbeValidNoIPSANs(frozenMetav1Now, 0),
+								sadWebhookConnectionValidNoIPSANs(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 								sadReadyCondition(frozenMetav1Now, 0),
 							},
@@ -1088,7 +1088,7 @@ func TestController(t *testing.T) {
 			wantSyncLoopErr:  testutil.WantExactErrorString(`cannot dial server: tls: failed to verify certificate: x509: cannot validate certificate for 127.0.0.1 because it doesn't contain any IP SANs`),
 		},
 		{
-			name:    "validateConnectionProbe: IPv6 address without port or brackets: should succeed since IPv6 brackets are optional without port",
+			name:    "validateConnection: IPv6 address without port or brackets: should succeed since IPv6 brackets are optional without port",
 			syncKey: controllerlib.Key{Name: "test-name"},
 			tlsDialerFunc: func(network string, addr string, config *tls.Config) (*tls.Conn, error) {
 				assert.Equal(t, "tcp", network)
@@ -1126,7 +1126,7 @@ func TestController(t *testing.T) {
 						Conditions: conditionstestutil.Replace(
 							allHappyConditionsSuccess("https://0:0:0:0:0:0:0:1/some/fake/path", frozenMetav1Now, 0),
 							[]metav1.Condition{
-								sadConnectionProbeValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
+								sadWebhookConnectionValidWithMessage(frozenMetav1Now, 0, "cannot dial server: IPv6 test fake error to skip real dial in prod code, this is actually success"),
 								sadReadyCondition(frozenMetav1Now, 0),
 								unknownAuthenticatorValid(frozenMetav1Now, 0),
 							},
@@ -1270,11 +1270,6 @@ func TestController(t *testing.T) {
 					"webhook": map[string]interface{}{
 						"name": "test-name",
 					},
-				}, {
-					"level":     "info",
-					"timestamp": "2099-08-08T13:57:36.123456Z",
-					"logger":    "webhookcachefiller-controller",
-					"message":   "ERROR: some update error",
 				},
 			},
 			wantActions: func() []coretesting.Action {
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index fd9893428..d3840fcad 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -67,7 +67,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 						Reason:  "InvalidTLSConfiguration",
 						Message: "invalid TLS configuration: illegal base64 data at input byte 7",
 					}, {
-						Type:    "ConnectionProbeValid",
+						Type:    "WebhookConnectionValid",
 						Status:  "Unknown",
 						Reason:  "UnableToValidate",
 						Message: "unable to validate; see other conditions for details",
@@ -98,7 +98,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 						Reason:  "UnableToValidate",
 						Message: "unable to validate; see other conditions for details",
 					}, {
-						Type:    "ConnectionProbeValid",
+						Type:    "WebhookConnectionValid",
 						Status:  "False",
 						Reason:  "UnableToDialServer",
 						Message: "cannot dial server: tls: failed to verify certificate: x509: certificate signed by unknown authority",
@@ -130,7 +130,7 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 						Reason:  "UnableToValidate",
 						Message: "unable to validate; see other conditions for details",
 					}, {
-						Type:    "ConnectionProbeValid",
+						Type:    "WebhookConnectionValid",
 						Status:  "False",
 						Reason:  "UnableToDialServer",
 						Message: "cannot dial server: dial tcp 127.0.0.1:443: connect: connection refused",
@@ -266,11 +266,6 @@ func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition {
 		Status:  "True",
 		Reason:  "Success",
 		Message: "authenticator initialized",
-	}, {
-		Type:    "ConnectionProbeValid",
-		Status:  "True",
-		Reason:  "Success",
-		Message: "tls verified",
 	}, {
 		Type:    "EndpointURLValid",
 		Status:  "True",
@@ -286,5 +281,10 @@ func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition {
 		Status:  "True",
 		Reason:  "Success",
 		Message: "successfully parsed specified CA bundle",
+	}, {
+		Type:    "WebhookConnectionValid",
+		Status:  "True",
+		Reason:  "Success",
+		Message: "tls verified",
 	}}
 }

From c6b0820438f12eeee87b6e2c31c49c6822a4c66c Mon Sep 17 00:00:00 2001
From: "Benjamin A. Petersen" <ben@benjaminapetersen.me>
Date: Tue, 26 Mar 2024 16:02:34 -0400
Subject: [PATCH 17/17] Fix some utils, spacing, func naming, test inputs, etc.

---
 cmd/pinniped/cmd/login_oidc_test.go           |  4 +-
 cmd/pinniped/cmd/login_static_test.go         |  4 +-
 .../jwtcachefiller/jwtcachefiller_test.go     | 27 +++++----
 .../webhookcachefiller_test.go                | 19 +++---
 .../conditionsutil/conditions_util.go         |  2 +-
 .../kubecertagent/kubecertagent_test.go       |  3 +-
 internal/testutil/stringutil.go               | 14 +++++
 internal/testutil/stringutil/stringutil.go    | 14 -----
 ...cierge_webhookauthenticator_status_test.go | 60 +++++++++++--------
 9 files changed, 79 insertions(+), 68 deletions(-)
 create mode 100644 internal/testutil/stringutil.go
 delete mode 100644 internal/testutil/stringutil/stringutil.go

diff --git a/cmd/pinniped/cmd/login_oidc_test.go b/cmd/pinniped/cmd/login_oidc_test.go
index 6d28d7b6e..42b84f9fa 100644
--- a/cmd/pinniped/cmd/login_oidc_test.go
+++ b/cmd/pinniped/cmd/login_oidc_test.go
@@ -21,7 +21,7 @@ import (
 	"go.pinniped.dev/internal/certauthority"
 	"go.pinniped.dev/internal/here"
 	"go.pinniped.dev/internal/plog"
-	"go.pinniped.dev/internal/testutil/stringutil"
+	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/pkg/conciergeclient"
 	"go.pinniped.dev/pkg/oidcclient"
 	"go.pinniped.dev/pkg/oidcclient/oidctypes"
@@ -596,7 +596,7 @@ func TestLoginOIDCCommand(t *testing.T) {
 			require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
 			require.Len(t, gotOptions, tt.wantOptionsCount)
 
-			require.Equal(t, tt.wantLogs, stringutil.SplitByNewline(buf.String()))
+			require.Equal(t, tt.wantLogs, testutil.SplitByNewline(buf.String()))
 		})
 	}
 }
diff --git a/cmd/pinniped/cmd/login_static_test.go b/cmd/pinniped/cmd/login_static_test.go
index bf65a4e39..19511edfc 100644
--- a/cmd/pinniped/cmd/login_static_test.go
+++ b/cmd/pinniped/cmd/login_static_test.go
@@ -20,7 +20,7 @@ import (
 	"go.pinniped.dev/internal/certauthority"
 	"go.pinniped.dev/internal/here"
 	"go.pinniped.dev/internal/plog"
-	"go.pinniped.dev/internal/testutil/stringutil"
+	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/pkg/conciergeclient"
 )
 
@@ -216,7 +216,7 @@ func TestLoginStaticCommand(t *testing.T) {
 			require.Equal(t, tt.wantStdout, stdout.String(), "unexpected stdout")
 			require.Equal(t, tt.wantStderr, stderr.String(), "unexpected stderr")
 
-			require.Equal(t, tt.wantLogs, stringutil.SplitByNewline(buf.String()))
+			require.Equal(t, tt.wantLogs, testutil.SplitByNewline(buf.String()))
 		})
 	}
 }
diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
index 8759f8437..3e1868f22 100644
--- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
+++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go
@@ -45,7 +45,6 @@ import (
 	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/internal/testutil/conciergetestutil"
 	"go.pinniped.dev/internal/testutil/conditionstestutil"
-	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/internal/testutil/tlsserver"
 )
 
@@ -654,21 +653,22 @@ func TestController(t *testing.T) {
 			jwtAuthenticators: []runtime.Object{
 				&auth1alpha1.JWTAuthenticator{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-name",
+						Name:       "test-name",
+						Generation: 1234,
 					},
 					Spec: *someJWTAuthenticatorSpec,
 					Status: auth1alpha1.JWTAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 1233),
 							[]metav1.Condition{
 								// sad and unknwn will update with new statuses and timestamps
-								sadReadyCondition(frozenTimeInThePast, 0),
-								sadDiscoveryURLValidx509(goodIssuer, frozenTimeInThePast, 0),
-								unknownAuthenticatorValid(frozenTimeInThePast, 0),
-								unknownJWKSURLValid(frozenTimeInThePast, 0),
-								unknownJWKSFetch(frozenTimeInThePast, 0),
+								sadReadyCondition(frozenTimeInThePast, 1232),
+								sadDiscoveryURLValidx509(goodIssuer, frozenTimeInThePast, 1231),
+								unknownAuthenticatorValid(frozenTimeInThePast, 1232),
+								unknownJWKSURLValid(frozenTimeInThePast, 1111),
+								unknownJWKSFetch(frozenTimeInThePast, 1122),
 								// this one will remain unchanged as it was good to begin with
-								happyTLSConfigurationValidCAParsed(frozenTimeInThePast, 0),
+								happyTLSConfigurationValidCAParsed(frozenTimeInThePast, 4321),
 							},
 						),
 						Phase: "Error",
@@ -688,15 +688,16 @@ func TestController(t *testing.T) {
 			wantActions: func() []coretesting.Action {
 				updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-name",
+						Name:       "test-name",
+						Generation: 1234,
 					},
 					Spec: *someJWTAuthenticatorSpec,
 					Status: auth1alpha1.JWTAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 1234),
 							[]metav1.Condition{
 								// this timestamp should not have updated, it didn't change.
-								happyTLSConfigurationValidCAParsed(frozenTimeInThePast, 0),
+								happyTLSConfigurationValidCAParsed(frozenTimeInThePast, 1234),
 							},
 						),
 						Phase: "Ready",
@@ -1703,7 +1704,7 @@ func TestController(t *testing.T) {
 				require.NoError(t, err)
 			}
 
-			actualLogLines := stringutil.SplitByNewline(log.String())
+			actualLogLines := testutil.SplitByNewline(log.String())
 			require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct")
 
 			for logLineNum, logLine := range actualLogLines {
diff --git a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
index cecd3e47e..081603236 100644
--- a/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
+++ b/internal/controller/authenticator/webhookcachefiller/webhookcachefiller_test.go
@@ -42,7 +42,6 @@ import (
 	"go.pinniped.dev/internal/testutil"
 	"go.pinniped.dev/internal/testutil/conciergetestutil"
 	"go.pinniped.dev/internal/testutil/conditionstestutil"
-	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/internal/testutil/tlsserver"
 )
 
@@ -450,15 +449,16 @@ func TestController(t *testing.T) {
 			webhooks: []runtime.Object{
 				&auth1alpha1.WebhookAuthenticator{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-name",
+						Name:       "test-name",
+						Generation: 1234,
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 666),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1233),
 							[]metav1.Condition{
-								sadReadyCondition(frozenTimeInThePast, 777),
-								happyEndpointURLValid(frozenTimeInThePast, 888),
+								sadReadyCondition(frozenTimeInThePast, 1232),
+								happyEndpointURLValid(frozenTimeInThePast, 1231),
 							},
 						),
 						Phase: "Ready",
@@ -480,14 +480,15 @@ func TestController(t *testing.T) {
 			wantActions: func() []coretesting.Action {
 				updateStatusAction := coretesting.NewUpdateAction(webhookAuthenticatorGVR, "", &auth1alpha1.WebhookAuthenticator{
 					ObjectMeta: metav1.ObjectMeta{
-						Name: "test-name",
+						Name:       "test-name",
+						Generation: 1234,
 					},
 					Spec: goodWebhookAuthenticatorSpecWithCA,
 					Status: auth1alpha1.WebhookAuthenticatorStatus{
 						Conditions: conditionstestutil.Replace(
-							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 0),
+							allHappyConditionsSuccess(goodWebhookDefaultServingCertEndpoint, frozenMetav1Now, 1234),
 							[]metav1.Condition{
-								happyEndpointURLValid(frozenTimeInThePast, 0),
+								happyEndpointURLValid(frozenTimeInThePast, 1234),
 							},
 						),
 						Phase: "Ready",
@@ -1333,7 +1334,7 @@ func TestController(t *testing.T) {
 			} else {
 				require.NoError(t, err)
 			}
-			actualLogLines := stringutil.SplitByNewline(log.String())
+			actualLogLines := testutil.SplitByNewline(log.String())
 			require.Equal(t, len(tt.wantLogs), len(actualLogLines), "log line count should be correct")
 
 			for logLineNum, logLine := range actualLogLines {
diff --git a/internal/controller/conditionsutil/conditions_util.go b/internal/controller/conditionsutil/conditions_util.go
index fa0cd8114..7412db764 100644
--- a/internal/controller/conditionsutil/conditions_util.go
+++ b/internal/controller/conditionsutil/conditions_util.go
@@ -66,7 +66,7 @@ func mergeIDPCondition(existing *[]metav1.Condition, new *metav1.Condition) bool
 	return false
 }
 
-// MergeConfigConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions.
+// MergeConfigConditions merges conditions into conditionsToUpdate. It returns true if it merged any error conditions.
 func MergeConfigConditions(conditions []*metav1.Condition, observedGeneration int64, conditionsToUpdate *[]metav1.Condition, log plog.MinLogger, now metav1.Time) bool {
 	hadErrorCondition := false
 	for i := range conditions {
diff --git a/internal/controller/kubecertagent/kubecertagent_test.go b/internal/controller/kubecertagent/kubecertagent_test.go
index c8dfd0fa9..2756e743b 100644
--- a/internal/controller/kubecertagent/kubecertagent_test.go
+++ b/internal/controller/kubecertagent/kubecertagent_test.go
@@ -38,7 +38,6 @@ import (
 	"go.pinniped.dev/internal/kubeclient"
 	"go.pinniped.dev/internal/plog"
 	"go.pinniped.dev/internal/testutil"
-	"go.pinniped.dev/internal/testutil/stringutil"
 	"go.pinniped.dev/test/testlib"
 )
 
@@ -1085,7 +1084,7 @@ func TestAgentController(t *testing.T) {
 			allAllowedErrors = append(allAllowedErrors, tt.alsoAllowUndesiredDistinctErrors...)
 			assert.Subsetf(t, allAllowedErrors, actualErrors, "actual errors contained additional error(s) which is not expected by the test")
 
-			assert.Equal(t, tt.wantDistinctLogs, deduplicate(stringutil.SplitByNewline(buf.String())), "unexpected logs")
+			assert.Equal(t, tt.wantDistinctLogs, deduplicate(testutil.SplitByNewline(buf.String())), "unexpected logs")
 
 			// Assert on all actions that happened to deployments.
 			var actualDeploymentActionVerbs []string
diff --git a/internal/testutil/stringutil.go b/internal/testutil/stringutil.go
new file mode 100644
index 000000000..35902de60
--- /dev/null
+++ b/internal/testutil/stringutil.go
@@ -0,0 +1,14 @@
+// Copyright 2024 the Pinniped contributors. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+package testutil
+
+import "strings"
+
+func SplitByNewline(lineToSplit string) []string {
+	if len(lineToSplit) == 0 {
+		return nil
+	}
+
+	return strings.Split(strings.TrimSpace(lineToSplit), "\n")
+}
diff --git a/internal/testutil/stringutil/stringutil.go b/internal/testutil/stringutil/stringutil.go
deleted file mode 100644
index aa8a266f1..000000000
--- a/internal/testutil/stringutil/stringutil.go
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright 2024 the Pinniped contributors. All Rights Reserved.
-// SPDX-License-Identifier: Apache-2.0
-
-package stringutil
-
-import "strings"
-
-func SplitByNewline(logs string) []string {
-	if len(logs) == 0 {
-		return nil
-	}
-
-	return strings.Split(strings.TrimSpace(logs), "\n")
-}
diff --git a/test/integration/concierge_webhookauthenticator_status_test.go b/test/integration/concierge_webhookauthenticator_status_test.go
index d3840fcad..1b372c39e 100644
--- a/test/integration/concierge_webhookauthenticator_status_test.go
+++ b/test/integration/concierge_webhookauthenticator_status_test.go
@@ -37,7 +37,8 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 			},
 			initialPhase:    v1alpha1.WebhookAuthenticatorPhaseReady,
 			finalConditions: allSuccessfulWebhookAuthenticatorConditions(),
-		}, {
+		},
+		{
 			name: "valid spec with invalid CA in TLS config will result in a WebhookAuthenticator that is not ready",
 			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
 				caBundleString := "invalid base64-encoded data"
@@ -74,7 +75,8 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 					},
 				},
 			),
-		}, {
+		},
+		{
 			name: "valid spec with valid CA in TLS config but does not match issuer server will result in a WebhookAuthenticator that is not ready",
 			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
 				webhookSpec := testEnv.TestWebhook.DeepCopy()
@@ -105,7 +107,8 @@ func TestConciergeWebhookAuthenticatorStatus_Parallel(t *testing.T) {
 					},
 				},
 			),
-		}, {
+		},
+		{
 			name: "invalid with unresponsive endpoint will result in a WebhookAuthenticator that is not ready",
 			spec: func() *v1alpha1.WebhookAuthenticatorSpec {
 				webhookSpec := testEnv.TestWebhook.DeepCopy()
@@ -223,7 +226,8 @@ func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) {
 					},
 				},
 			},
-		}, {
+		},
+		{
 			// since the CRD validations do not assess fitness of the value provided
 			name: "valid authenticator can have TLS CertificateAuthorityData string that is an invalid certificate",
 			webhookAuthenticator: &v1alpha1.WebhookAuthenticator{
@@ -260,31 +264,37 @@ func TestConciergeWebhookAuthenticatorCRDValidations_Parallel(t *testing.T) {
 		})
 	}
 }
+
 func allSuccessfulWebhookAuthenticatorConditions() []metav1.Condition {
 	return []metav1.Condition{{
 		Type:    "AuthenticatorValid",
 		Status:  "True",
 		Reason:  "Success",
 		Message: "authenticator initialized",
-	}, {
-		Type:    "EndpointURLValid",
-		Status:  "True",
-		Reason:  "Success",
-		Message: "endpoint is a valid URL",
-	}, {
-		Type:    "Ready",
-		Status:  "True",
-		Reason:  "Success",
-		Message: "the WebhookAuthenticator is ready",
-	}, {
-		Type:    "TLSConfigurationValid",
-		Status:  "True",
-		Reason:  "Success",
-		Message: "successfully parsed specified CA bundle",
-	}, {
-		Type:    "WebhookConnectionValid",
-		Status:  "True",
-		Reason:  "Success",
-		Message: "tls verified",
-	}}
+	},
+		{
+			Type:    "EndpointURLValid",
+			Status:  "True",
+			Reason:  "Success",
+			Message: "endpoint is a valid URL",
+		},
+		{
+			Type:    "Ready",
+			Status:  "True",
+			Reason:  "Success",
+			Message: "the WebhookAuthenticator is ready",
+		},
+		{
+			Type:    "TLSConfigurationValid",
+			Status:  "True",
+			Reason:  "Success",
+			Message: "successfully parsed specified CA bundle",
+		},
+		{
+			Type:    "WebhookConnectionValid",
+			Status:  "True",
+			Reason:  "Success",
+			Message: "tls verified",
+		},
+	}
 }