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,295 @@ 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", }, }, }, - 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...", + 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: "", + 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: "valid webhook will complete sync loop successfully with success conditions and ready phase", + 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, + }, + }, + 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, + }, + { + 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{ @@ -188,18 +618,283 @@ func TestController(t *testing.T) { Name: "test-name", }, Spec: auth1alpha1.WebhookAuthenticatorSpec{ - Endpoint: goodEndpoint, - TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: ""}, + Endpoint: badEndpoint, }, }, }, - // 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: 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: "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: "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{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + }, + Spec: auth1alpha1.WebhookAuthenticatorSpec{ + 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", + }, + }, + }, + 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") + actualLogLines := testlogger.LogLines(log.String()) + require.Equal(t, len(actualLogLines), len(tt.wantLogs), "log line count 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") + 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"