From 5aa0d9126779496a3f9e2e631be8533301fa897e Mon Sep 17 00:00:00 2001 From: Ryan Richard Date: Fri, 17 Jun 2022 12:56:53 -0400 Subject: [PATCH] New controller watches OIDCClients and updates validation Conditions --- .../config/v1alpha1/types_meta.go.tmpl | 75 ++ .../config/v1alpha1/types_oidcclient.go.tmpl | 28 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ deploy/supervisor/rbac.yaml | 8 + generated/1.17/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.18/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.19/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.20/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.21/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.22/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.23/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ generated/1.24/README.adoc | 38 + .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- ...g.supervisor.pinniped.dev_oidcclients.yaml | 76 ++ .../supervisor/config/v1alpha1/types_meta.go | 75 ++ .../config/v1alpha1/types_oidcclient.go | 28 +- .../config/v1alpha1/zz_generated.deepcopy.go | 26 +- .../conditionsutil/conditions_util.go | 123 +++ .../conditionsutil/conditions_util.go.go | 68 -- .../active_directory_upstream_watcher.go | 2 +- .../ldap_upstream_watcher.go | 2 +- .../oidcclientwatcher/oidc_client_watcher.go | 317 ++++++ .../oidc_client_watcher_test.go | 903 ++++++++++++++++++ .../oidc_upstream_watcher.go | 2 +- internal/crud/crud.go | 13 +- .../oidcclientsecretstorage.go | 67 ++ .../oidcclientsecretstorage_test.go | 125 +++ internal/supervisor/server/server.go | 11 + ...test.go => supervisor_oidc_client_test.go} | 203 ++++ 59 files changed, 3980 insertions(+), 116 deletions(-) create mode 100644 apis/supervisor/config/v1alpha1/types_meta.go.tmpl create mode 100644 generated/1.17/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.18/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.19/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.20/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.21/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.22/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.23/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/1.24/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 generated/latest/apis/supervisor/config/v1alpha1/types_meta.go create mode 100644 internal/controller/conditionsutil/conditions_util.go delete mode 100644 internal/controller/conditionsutil/conditions_util.go.go create mode 100644 internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go create mode 100644 internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher_test.go create mode 100644 internal/oidcclientsecretstorage/oidcclientsecretstorage.go create mode 100644 internal/oidcclientsecretstorage/oidcclientsecretstorage_test.go rename test/integration/{oidc_client_test.go => supervisor_oidc_client_test.go} (66%) diff --git a/apis/supervisor/config/v1alpha1/types_meta.go.tmpl b/apis/supervisor/config/v1alpha1/types_meta.go.tmpl new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/apis/supervisor/config/v1alpha1/types_meta.go.tmpl @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl b/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl index 17a1103f..1bc7399d 100644 --- a/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl +++ b/apis/supervisor/config/v1alpha1/types_oidcclient.go.tmpl @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml b/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/deploy/supervisor/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + enum: + - Pending + - Ready + - Error + type: string type: object required: - spec diff --git a/deploy/supervisor/rbac.yaml b/deploy/supervisor/rbac.yaml index a56818fe..97b542fe 100644 --- a/deploy/supervisor/rbac.yaml +++ b/deploy/supervisor/rbac.yaml @@ -24,6 +24,14 @@ rules: - #@ pinnipedDevAPIGroupWithPrefix("config.supervisor") resources: [federationdomains/status] verbs: [get, patch, update] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("config.supervisor") + resources: [oidcclients] + verbs: [get, list, watch] + - apiGroups: + - #@ pinnipedDevAPIGroupWithPrefix("config.supervisor") + resources: [oidcclients/status] + verbs: [get, patch, update] - apiGroups: - #@ pinnipedDevAPIGroupWithPrefix("idp.supervisor") resources: [oidcidentityproviders] diff --git a/generated/1.17/README.adoc b/generated/1.17/README.adoc index 4d70f8a7..2b29fc45 100644 --- a/generated/1.17/README.adoc +++ b/generated/1.17/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.17/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-17-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.17/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.17/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.17/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.17/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.17/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.17/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.17/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.17/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.17/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + enum: + - Pending + - Ready + - Error + type: string type: object required: - spec diff --git a/generated/1.18/README.adoc b/generated/1.18/README.adoc index a987b55c..e2fb5b80 100644 --- a/generated/1.18/README.adoc +++ b/generated/1.18/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-18-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.18/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.18/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.18/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.18/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.18/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.18/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.18/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.18/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.18/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + enum: + - Pending + - Ready + - Error + type: string type: object required: - spec diff --git a/generated/1.19/README.adoc b/generated/1.19/README.adoc index df1fdef2..337689da 100644 --- a/generated/1.19/README.adoc +++ b/generated/1.19/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.19/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-19-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.19/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.19/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.19/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.19/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.19/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.19/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.19/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.19/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.19/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + enum: + - Pending + - Ready + - Error + type: string type: object required: - spec diff --git a/generated/1.20/README.adoc b/generated/1.20/README.adoc index f570511d..493e4ba2 100644 --- a/generated/1.20/README.adoc +++ b/generated/1.20/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.2/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-20-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.20/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.20/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.20/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.20/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.20/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.20/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.20/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.20/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.20/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.20/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.20/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.20/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.20/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.20/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.20/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + 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 768478db..59be6db3 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.21/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.21/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + 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 8212b9b8..7f4ace33 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.22/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.22/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + 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 e67a0344..ad7d96a6 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.23/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.23/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + 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 73c3b4bf..9a7ab440 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -575,6 +575,28 @@ Package v1alpha1 is the v1alpha1 version of the Pinniped supervisor configuratio +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-condition"] +==== Condition + +Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API version we can switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclientstatus[$$OIDCClientStatus$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`type`* __string__ | type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) +| *`status`* __ConditionStatus__ | status of the condition, one of True, False, Unknown. +| *`observedGeneration`* __integer__ | observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. +| *`lastTransitionTime`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#time-v1-meta[$$Time$$]__ | lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. +| *`reason`* __string__ | reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. +| *`message`* __string__ | message is a human readable message indicating details about the transition. This may be an empty string. +|=== + + [id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-federationdomain"] ==== FederationDomain @@ -720,6 +742,22 @@ OIDCClientSpec is a struct that describes an OIDC Client. |=== +[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclientstatus"] +==== OIDCClientStatus + +OIDCClientStatus is a struct that describes the actual state of an OIDCClient. + +.Appears In: +**** +- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-oidcclient[$$OIDCClient$$] +**** + +[cols="25a,75a", options="header"] +|=== +| Field | Description +| *`phase`* __OIDCClientPhase__ | Phase summarizes the overall status of the OIDCClient. +| *`conditions`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-supervisor-config-v1alpha1-condition[$$Condition$$] array__ | Represents the observations of an OIDCClient's current state. +|=== diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/types_meta.go b/generated/1.24/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/1.24/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml b/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml index 6030582f..b5569275 100644 --- a/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml +++ b/generated/1.24/crds/config.supervisor.pinniped.dev_oidcclients.yaml @@ -115,6 +115,82 @@ spec: type: object status: description: Status of the OIDC client. + properties: + conditions: + description: Represents the observations of an OIDCClient's current + state. + items: + description: Condition status of a resource (mirrored from the metav1.Condition + type added in Kubernetes 1.19). In a future API version we can + switch to using the upstream type. See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + phase: + default: Pending + description: Phase summarizes the overall status of the OIDCClient. + enum: + - Pending + - Ready + - Error + type: string type: object required: - spec diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_meta.go b/generated/latest/apis/supervisor/config/v1alpha1/types_meta.go new file mode 100644 index 00000000..cd46a471 --- /dev/null +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_meta.go @@ -0,0 +1,75 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +// ConditionStatus is effectively an enum type for Condition.Status. +type ConditionStatus string + +// These are valid condition statuses. "ConditionTrue" means a resource is in the condition. +// "ConditionFalse" means a resource is not in the condition. "ConditionUnknown" means kubernetes +// can't decide if a resource is in the condition or not. In the future, we could add other +// intermediate conditions, e.g. ConditionDegraded. +const ( + ConditionTrue ConditionStatus = "True" + ConditionFalse ConditionStatus = "False" + ConditionUnknown ConditionStatus = "Unknown" +) + +// Condition status of a resource (mirrored from the metav1.Condition type added in Kubernetes 1.19). In a future API +// version we can switch to using the upstream type. +// See https://github.com/kubernetes/apimachinery/blob/v0.19.0/pkg/apis/meta/v1/types.go#L1353-L1413. +type Condition struct { + // type of condition in CamelCase or in foo.example.com/CamelCase. + // --- + // Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be + // useful (see .node.status.conditions), the ability to deconflict is important. + // The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$` + // +kubebuilder:validation:MaxLength=316 + Type string `json:"type"` + + // status of the condition, one of True, False, Unknown. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Enum=True;False;Unknown + Status ConditionStatus `json:"status"` + + // observedGeneration represents the .metadata.generation that the condition was set based upon. + // For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + // with respect to the current state of the instance. + // +optional + // +kubebuilder:validation:Minimum=0 + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // lastTransitionTime is the last time the condition transitioned from one status to another. + // This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:Type=string + // +kubebuilder:validation:Format=date-time + LastTransitionTime metav1.Time `json:"lastTransitionTime"` + + // reason contains a programmatic identifier indicating the reason for the condition's last transition. + // Producers of specific condition types may define expected values and meanings for this field, + // and whether the values are considered a guaranteed API. + // The value should be a CamelCase string. + // This field may not be empty. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=1024 + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:Pattern=`^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$` + Reason string `json:"reason"` + + // message is a human readable message indicating details about the transition. + // This may be an empty string. + // +required + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32768 + Message string `json:"message"` +} diff --git a/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go b/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go index 17a1103f..1bc7399d 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/types_oidcclient.go @@ -3,8 +3,19 @@ package v1alpha1 -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +type OIDCClientPhase string + +const ( + // PhasePending is the default phase for newly-created OIDCClient resources. + PhasePending OIDCClientPhase = "Pending" + + // PhaseReady is the phase for an OIDCClient resource in a healthy state. + PhaseReady OIDCClientPhase = "Ready" + + // PhaseError is the phase for an OIDCClient in an unhealthy state. + PhaseError OIDCClientPhase = "Error" ) // +kubebuilder:validation:Pattern=`^https://.+|^http://(127\.0\.0\.1|\[::1\])(:\d+)?/` @@ -62,8 +73,19 @@ type OIDCClientSpec struct { AllowedScopes []Scope `json:"allowedScopes"` } -// OIDCClientStatus is a struct that describes the actual state of an OIDC Client. +// OIDCClientStatus is a struct that describes the actual state of an OIDCClient. type OIDCClientStatus struct { + // Phase summarizes the overall status of the OIDCClient. + // +kubebuilder:default=Pending + // +kubebuilder:validation:Enum=Pending;Ready;Error + Phase OIDCClientPhase `json:"phase,omitempty"` + + // Represents the observations of an OIDCClient's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +listType=map + // +listMapKey=type + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"` } // OIDCClient describes the configuration of an OIDC client. diff --git a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go index f4468886..3e7f07d0 100644 --- a/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go +++ b/generated/latest/apis/supervisor/config/v1alpha1/zz_generated.deepcopy.go @@ -12,6 +12,23 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FederationDomain) DeepCopyInto(out *FederationDomain) { *out = *in @@ -157,7 +174,7 @@ func (in *OIDCClient) DeepCopyInto(out *OIDCClient) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -246,6 +263,13 @@ func (in *OIDCClientSpec) DeepCopy() *OIDCClientSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OIDCClientStatus) DeepCopyInto(out *OIDCClientStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } diff --git a/internal/controller/conditionsutil/conditions_util.go b/internal/controller/conditionsutil/conditions_util.go new file mode 100644 index 00000000..431c1052 --- /dev/null +++ b/internal/controller/conditionsutil/conditions_util.go @@ -0,0 +1,123 @@ +// Copyright 2021-2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package conditionsutil + +import ( + "sort" + + "k8s.io/apimachinery/pkg/api/equality" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" + "go.pinniped.dev/internal/plog" +) + +// MergeIDPConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. +func MergeIDPConditions(conditions []*idpv1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]idpv1alpha1.Condition, log plog.MinLogger) bool { + hadErrorCondition := false + for i := range conditions { + cond := conditions[i].DeepCopy() + cond.LastTransitionTime = v1.Now() + cond.ObservedGeneration = observedGeneration + if mergeIDPCondition(conditionsToUpdate, cond) { + log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) + } + if cond.Status == idpv1alpha1.ConditionFalse { + hadErrorCondition = true + } + } + sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { + return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type + }) + return hadErrorCondition +} + +// mergeIDPCondition merges a new idpv1alpha1.Condition into a slice of existing conditions. It returns true +// if the condition has meaningfully changed. +func mergeIDPCondition(existing *[]idpv1alpha1.Condition, new *idpv1alpha1.Condition) bool { + // Find any existing condition with a matching type. + var old *idpv1alpha1.Condition + for i := range *existing { + if (*existing)[i].Type == new.Type { + old = &(*existing)[i] + continue + } + } + + // If there is no existing condition of this type, append this one and we're done. + if old == nil { + *existing = append(*existing, *new) + return true + } + + // Set the LastTransitionTime depending on whether the status has changed. + new = new.DeepCopy() + if old.Status == new.Status { + new.LastTransitionTime = old.LastTransitionTime + } + + // If anything has actually changed, update the entry and return true. + if !equality.Semantic.DeepEqual(old, new) { + *old = *new + return true + } + + // Otherwise the entry is already up to date. + return false +} + +// MergeConfigConditions merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. +func MergeConfigConditions(conditions []*configv1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]configv1alpha1.Condition, log plog.MinLogger) bool { + hadErrorCondition := false + for i := range conditions { + cond := conditions[i].DeepCopy() + cond.LastTransitionTime = v1.Now() + cond.ObservedGeneration = observedGeneration + if mergeConfigCondition(conditionsToUpdate, cond) { + log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) + } + if cond.Status == configv1alpha1.ConditionFalse { + hadErrorCondition = true + } + } + sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { + return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type + }) + return hadErrorCondition +} + +// mergeConfigCondition merges a new idpv1alpha1.Condition into a slice of existing conditions. It returns true +// if the condition has meaningfully changed. +func mergeConfigCondition(existing *[]configv1alpha1.Condition, new *configv1alpha1.Condition) bool { + // Find any existing condition with a matching type. + var old *configv1alpha1.Condition + for i := range *existing { + if (*existing)[i].Type == new.Type { + old = &(*existing)[i] + continue + } + } + + // If there is no existing condition of this type, append this one and we're done. + if old == nil { + *existing = append(*existing, *new) + return true + } + + // Set the LastTransitionTime depending on whether the status has changed. + new = new.DeepCopy() + if old.Status == new.Status { + new.LastTransitionTime = old.LastTransitionTime + } + + // If anything has actually changed, update the entry and return true. + if !equality.Semantic.DeepEqual(old, new) { + *old = *new + return true + } + + // Otherwise the entry is already up to date. + return false +} diff --git a/internal/controller/conditionsutil/conditions_util.go.go b/internal/controller/conditionsutil/conditions_util.go.go deleted file mode 100644 index dec4695a..00000000 --- a/internal/controller/conditionsutil/conditions_util.go.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2021 the Pinniped contributors. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -package conditionsutil - -import ( - "sort" - - "k8s.io/apimachinery/pkg/api/equality" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1" - "go.pinniped.dev/internal/plog" -) - -// Merge merges conditions into conditionsToUpdate. If returns true if it merged any error conditions. -func Merge(conditions []*v1alpha1.Condition, observedGeneration int64, conditionsToUpdate *[]v1alpha1.Condition, log plog.MinLogger) bool { - hadErrorCondition := false - for i := range conditions { - cond := conditions[i].DeepCopy() - cond.LastTransitionTime = v1.Now() - cond.ObservedGeneration = observedGeneration - if mergeCondition(conditionsToUpdate, cond) { - log.Info("updated condition", "type", cond.Type, "status", cond.Status, "reason", cond.Reason, "message", cond.Message) - } - if cond.Status == v1alpha1.ConditionFalse { - hadErrorCondition = true - } - } - sort.SliceStable(*conditionsToUpdate, func(i, j int) bool { - return (*conditionsToUpdate)[i].Type < (*conditionsToUpdate)[j].Type - }) - return hadErrorCondition -} - -// mergeCondition merges a new v1alpha1.Condition into a slice of existing conditions. It returns true -// if the condition has meaningfully changed. -func mergeCondition(existing *[]v1alpha1.Condition, new *v1alpha1.Condition) bool { - // Find any existing condition with a matching type. - var old *v1alpha1.Condition - for i := range *existing { - if (*existing)[i].Type == new.Type { - old = &(*existing)[i] - continue - } - } - - // If there is no existing condition of this type, append this one and we're done. - if old == nil { - *existing = append(*existing, *new) - return true - } - - // Set the LastTransitionTime depending on whether the status has changed. - new = new.DeepCopy() - if old.Status == new.Status { - new.LastTransitionTime = old.LastTransitionTime - } - - // If anything has actually changed, update the entry and return true. - if !equality.Semantic.DeepEqual(old, new) { - *old = *new - return true - } - - // Otherwise the entry is already up to date. - return false -} diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index 4aaa41b9..5fd198ea 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -362,7 +362,7 @@ func (c *activeDirectoryWatcherController) updateStatus(ctx context.Context, ups log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.MergeIDPConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) updated.Status.Phase = v1alpha1.ActiveDirectoryPhaseReady if hadErrorCondition { diff --git a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go index a942bbf9..6d370e26 100644 --- a/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go +++ b/internal/controller/supervisorconfig/ldapupstreamwatcher/ldap_upstream_watcher.go @@ -255,7 +255,7 @@ func (c *ldapWatcherController) updateStatus(ctx context.Context, upstream *v1al log := plog.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.MergeIDPConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) updated.Status.Phase = v1alpha1.LDAPPhaseReady if hadErrorCondition { diff --git a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go new file mode 100644 index 00000000..600f7420 --- /dev/null +++ b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher.go @@ -0,0 +1,317 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidcclientwatcher + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + corev1informers "k8s.io/client-go/informers/core/v1" + + "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + pinnipedclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned" + configInformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions/config/v1alpha1" + pinnipedcontroller "go.pinniped.dev/internal/controller" + "go.pinniped.dev/internal/controller/conditionsutil" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/oidcclientsecretstorage" + "go.pinniped.dev/internal/plog" +) + +const ( + clientSecretExists = "ClientSecretExists" + allowedGrantTypesValid = "AllowedGrantTypesValid" + allowedScopesValid = "AllowedScopesValid" + + reasonSuccess = "Success" + reasonMissingRequiredValue = "MissingRequiredValue" + reasonNoClientSecretFound = "NoClientSecretFound" + + authorizationCodeGrantTypeName = "authorization_code" + refreshTokenGrantTypeName = "refresh_token" + tokenExchangeGrantTypeName = "urn:ietf:params:oauth:grant-type:token-exchange" //nolint:gosec // this is not a credential + + openidScopeName = "openid" + offlineAccessScopeName = "offline_access" + requestAudienceScopeName = "pinniped:request-audience" + usernameScopeName = "username" + groupsScopeName = "groups" + + allowedGrantTypesFieldName = "allowedGrantTypes" + allowedScopesFieldName = "allowedScopes" + + secretTypeToObserve = "storage.pinniped.dev/oidc-client-secret" //nolint:gosec // this is not a credential +) + +type oidcClientWatcherController struct { + pinnipedClient pinnipedclientset.Interface + oidcClientInformer configInformers.OIDCClientInformer + secretInformer corev1informers.SecretInformer +} + +// NewOIDCClientWatcherController returns a controllerlib.Controller that watches OIDCClients and updates +// their status with validation errors. +func NewOIDCClientWatcherController( + pinnipedClient pinnipedclientset.Interface, + secretInformer corev1informers.SecretInformer, + oidcClientInformer configInformers.OIDCClientInformer, + withInformer pinnipedcontroller.WithInformerOptionFunc, +) controllerlib.Controller { + return controllerlib.New( + controllerlib.Config{ + Name: "OIDCClientWatcherController", + Syncer: &oidcClientWatcherController{ + pinnipedClient: pinnipedClient, + secretInformer: secretInformer, + oidcClientInformer: oidcClientInformer, + }, + }, + // We want to be notified when an OIDCClient's corresponding secret gets updated or deleted. + withInformer( + secretInformer, + pinnipedcontroller.MatchAnySecretOfTypeFilter(secretTypeToObserve, pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + // We want to be notified when anything happens to an OIDCClient. + withInformer( + oidcClientInformer, + pinnipedcontroller.MatchAnythingFilter(pinnipedcontroller.SingletonQueue()), + controllerlib.InformerOption{}, + ), + ) +} + +// Sync implements controllerlib.Syncer. +func (c *oidcClientWatcherController) Sync(ctx controllerlib.Context) error { + // Sync could be called on either a Secret or an OIDCClient, so to keep it simple, revalidate + // all OIDCClients whenever anything changes. + oidcClients, err := c.oidcClientInformer.Lister().List(labels.Everything()) + if err != nil { + return fmt.Errorf("failed to list OIDCClients: %w", err) + } + + // We're only going to use storage to call GetName(), which happens to not need the constructor params. + // This is because we can read the Secrets from the informer cache here, instead of doing live reads. + storage := oidcclientsecretstorage.New(nil, nil) + + for _, oidcClient := range oidcClients { + correspondingSecretName := storage.GetName(oidcClient.UID) + + secret, err := c.secretInformer.Lister().Secrets(oidcClient.Namespace).Get(correspondingSecretName) + if err != nil { + if !k8serrors.IsNotFound(err) { + // Anything other than a NotFound error is unexpected when reading from an informer. + return fmt.Errorf("failed to get %s/%s secret: %w", oidcClient.Namespace, correspondingSecretName, err) + } + // Got a NotFound error, so continue. The Secret just doesn't exist yet, which is okay. + plog.DebugErr( + "OIDCClientWatcherController error getting storage Secret for OIDCClient's client secrets", err, + "oidcClientName", oidcClient.Name, + "oidcClientNamespace", oidcClient.Namespace, + "secretName", correspondingSecretName, + ) + secret = nil + } + + conditions := validateOIDCClient(oidcClient, secret) + + if err := c.updateStatus(ctx.Context, oidcClient, conditions); err != nil { + return fmt.Errorf("cannot update OIDCClient '%s/%s': %w", oidcClient.Namespace, oidcClient.Name, err) + } + + plog.Debug( + "OIDCClientWatcherController Sync updated an OIDCClient", + "oidcClientName", oidcClient.Name, + "oidcClientNamespace", oidcClient.Namespace, + "conditionsCount", len(conditions), + ) + } + + return nil +} + +// validateOIDCClient validates the OIDCClient and its corresponding client secret storage Secret. +// When the corresponding client secret storage Secret was not found, pass nil to this function to +// get the validation error for that case. +func validateOIDCClient(oidcClient *v1alpha1.OIDCClient, secret *v1.Secret) []*v1alpha1.Condition { + c := validateSecret(secret, []*v1alpha1.Condition{}) + c = validateAllowedGrantTypes(oidcClient, c) + c = validateAllowedScopes(oidcClient, c) + return c +} + +// validateAllowedScopes checks if allowedScopes is valid on the OIDCClient. +func validateAllowedScopes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) []*v1alpha1.Condition { + switch { + case !allowedScopesContains(oidcClient, openidScopeName): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedScopesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q must always be included in %q", openidScopeName, allowedScopesFieldName), + }) + case allowedGrantTypesContains(oidcClient, refreshTokenGrantTypeName) && !allowedScopesContains(oidcClient, offlineAccessScopeName): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedScopesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q must be included in %q when %q is included in %q", + offlineAccessScopeName, allowedScopesFieldName, refreshTokenGrantTypeName, allowedGrantTypesFieldName), + }) + case allowedScopesContains(oidcClient, requestAudienceScopeName) && + (!allowedScopesContains(oidcClient, usernameScopeName) || !allowedScopesContains(oidcClient, groupsScopeName)): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedScopesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q and %q must be included in %q when %q is included in %q", + usernameScopeName, groupsScopeName, allowedScopesFieldName, requestAudienceScopeName, allowedScopesFieldName), + }) + case allowedGrantTypesContains(oidcClient, tokenExchangeGrantTypeName) && !allowedScopesContains(oidcClient, requestAudienceScopeName): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedScopesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q must be included in %q when %q is included in %q", + requestAudienceScopeName, allowedScopesFieldName, tokenExchangeGrantTypeName, allowedGrantTypesFieldName), + }) + default: + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedScopesValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: fmt.Sprintf("%q is valid", allowedScopesFieldName), + }) + } + return conditions +} + +// validateAllowedGrantTypes checks if allowedGrantTypes is valid on the OIDCClient. +func validateAllowedGrantTypes(oidcClient *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) []*v1alpha1.Condition { + switch { + case !allowedGrantTypesContains(oidcClient, authorizationCodeGrantTypeName): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedGrantTypesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q must always be included in %q", + authorizationCodeGrantTypeName, allowedGrantTypesFieldName), + }) + case allowedScopesContains(oidcClient, offlineAccessScopeName) && !allowedGrantTypesContains(oidcClient, refreshTokenGrantTypeName): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedGrantTypesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q must be included in %q when %q is included in %q", + refreshTokenGrantTypeName, allowedGrantTypesFieldName, offlineAccessScopeName, allowedScopesFieldName), + }) + case allowedScopesContains(oidcClient, requestAudienceScopeName) && !allowedGrantTypesContains(oidcClient, tokenExchangeGrantTypeName): + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedGrantTypesValid, + Status: v1alpha1.ConditionFalse, + Reason: reasonMissingRequiredValue, + Message: fmt.Sprintf("%q must be included in %q when %q is included in %q", + tokenExchangeGrantTypeName, allowedGrantTypesFieldName, requestAudienceScopeName, allowedScopesFieldName), + }) + default: + conditions = append(conditions, &v1alpha1.Condition{ + Type: allowedGrantTypesValid, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: fmt.Sprintf("%q is valid", allowedGrantTypesFieldName), + }) + } + return conditions +} + +// validateSecret checks if the client secret storage Secret is valid and contains at least one client secret. +func validateSecret(secret *v1.Secret, conditions []*v1alpha1.Condition) []*v1alpha1.Condition { + if secret == nil { + // Invalid: no storage Secret found. + conditions = append(conditions, &v1alpha1.Condition{ + Type: clientSecretExists, + Status: v1alpha1.ConditionFalse, + Reason: reasonNoClientSecretFound, + Message: "no client secret found (no Secret storage found)", + }) + return conditions + } + + storedClientSecret, err := oidcclientsecretstorage.ReadFromSecret(secret) + if err != nil { + // Invalid: storage Secret exists but its data could not be parsed. + conditions = append(conditions, &v1alpha1.Condition{ + Type: clientSecretExists, + Status: v1alpha1.ConditionFalse, + Reason: reasonNoClientSecretFound, + Message: fmt.Sprintf("error reading client secret storage: %s", err.Error()), + }) + return conditions + } + + // Successfully read the stored client secrets, so check if there are any stored in the list. + storedClientSecretsCount := len(storedClientSecret.SecretHashes) + if storedClientSecretsCount == 0 { + // Invalid: no client secrets stored. + conditions = append(conditions, &v1alpha1.Condition{ + Type: clientSecretExists, + Status: v1alpha1.ConditionFalse, + Reason: reasonNoClientSecretFound, + Message: "no client secret found (empty list in storage)", + }) + } else { + // Valid: has at least one client secret stored for this OIDC client. + conditions = append(conditions, &v1alpha1.Condition{ + Type: clientSecretExists, + Status: v1alpha1.ConditionTrue, + Reason: reasonSuccess, + Message: fmt.Sprintf("%d client secret(s) found", storedClientSecretsCount), + }) + } + return conditions +} + +func allowedGrantTypesContains(haystack *v1alpha1.OIDCClient, needle string) bool { + for _, hay := range haystack.Spec.AllowedGrantTypes { + if hay == v1alpha1.GrantType(needle) { + return true + } + } + return false +} + +func allowedScopesContains(haystack *v1alpha1.OIDCClient, needle string) bool { + for _, hay := range haystack.Spec.AllowedScopes { + if hay == v1alpha1.Scope(needle) { + return true + } + } + return false +} + +func (c *oidcClientWatcherController) updateStatus(ctx context.Context, upstream *v1alpha1.OIDCClient, conditions []*v1alpha1.Condition) error { + updated := upstream.DeepCopy() + + hadErrorCondition := conditionsutil.MergeConfigConditions(conditions, upstream.Generation, &updated.Status.Conditions, plog.New()) + + updated.Status.Phase = v1alpha1.PhaseReady + if hadErrorCondition { + updated.Status.Phase = v1alpha1.PhaseError + } + + if equality.Semantic.DeepEqual(upstream, updated) { + return nil + } + + _, err := c.pinnipedClient. + ConfigV1alpha1(). + OIDCClients(upstream.Namespace). + UpdateStatus(ctx, updated, metav1.UpdateOptions{}) + return err +} diff --git a/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher_test.go b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher_test.go new file mode 100644 index 00000000..683c92ab --- /dev/null +++ b/internal/controller/supervisorconfig/oidcclientwatcher/oidc_client_watcher_test.go @@ -0,0 +1,903 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidcclientwatcher + +import ( + "context" + "encoding/base32" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + kubeinformers "k8s.io/client-go/informers" + kubernetesfake "k8s.io/client-go/kubernetes/fake" + + configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake" + pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" + "go.pinniped.dev/internal/controllerlib" + "go.pinniped.dev/internal/testutil" +) + +func TestOIDCClientWatcherControllerFilterSecret(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + secret metav1.Object + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "a secret of the right type", + secret: &corev1.Secret{ + Type: "storage.pinniped.dev/oidc-client-secret", + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + { + name: "a secret of the wrong type", + secret: &corev1.Secret{ + Type: "secrets.pinniped.dev/some-other-type", + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + { + name: "resource of wrong data type", + secret: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: "some-name", Namespace: "some-namespace"}, + }, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + oidcClientsInformer := pinnipedinformers.NewSharedInformerFactory( + pinnipedfake.NewSimpleClientset(), + 0, + ).Config().V1alpha1().OIDCClients() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewOIDCClientWatcherController( + nil, // pinnipedClient, not needed + secretInformer, + oidcClientsInformer, + withInformer.WithInformer, + ) + + unrelated := corev1.Secret{} + filter := withInformer.GetFilterForInformer(secretInformer) + require.Equal(t, tt.wantAdd, filter.Add(tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(&unrelated, tt.secret)) + require.Equal(t, tt.wantUpdate, filter.Update(tt.secret, &unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(tt.secret)) + }) + } +} + +func TestOIDCClientWatcherControllerFilterOIDCClient(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + oidcClient configv1alpha1.OIDCClient + wantAdd bool + wantUpdate bool + wantDelete bool + }{ + { + name: "anything goes", + oidcClient: configv1alpha1.OIDCClient{}, + wantAdd: true, + wantUpdate: true, + wantDelete: true, + }, + } + for _, test := range tests { + tt := test + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + secretInformer := kubeinformers.NewSharedInformerFactory( + kubernetesfake.NewSimpleClientset(), + 0, + ).Core().V1().Secrets() + oidcClientsInformer := pinnipedinformers.NewSharedInformerFactory( + pinnipedfake.NewSimpleClientset(), + 0, + ).Config().V1alpha1().OIDCClients() + withInformer := testutil.NewObservableWithInformerOption() + _ = NewOIDCClientWatcherController( + nil, // pinnipedClient, not needed + secretInformer, + oidcClientsInformer, + withInformer.WithInformer, + ) + + unrelated := configv1alpha1.OIDCClient{} + filter := withInformer.GetFilterForInformer(oidcClientsInformer) + require.Equal(t, tt.wantAdd, filter.Add(&tt.oidcClient)) + require.Equal(t, tt.wantUpdate, filter.Update(&unrelated, &tt.oidcClient)) + require.Equal(t, tt.wantUpdate, filter.Update(&tt.oidcClient, &unrelated)) + require.Equal(t, tt.wantDelete, filter.Delete(&tt.oidcClient)) + }) + } +} + +func TestOIDCClientWatcherControllerSync(t *testing.T) { + t.Parallel() + + const ( + testName = "test-name" + testNamespace = "test-namespace" + testUID = "test-uid-123" + + //nolint:gosec // this is not a credential + testBcryptSecret1 = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password1" + + //nolint:gosec // this is not a credential + testBcryptSecret2 = "$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m" // bcrypt of "password2" + ) + + now := metav1.NewTime(time.Now().UTC()) + earlier := metav1.NewTime(now.Add(-1 * time.Hour).UTC()) + + happyAllowedGrantTypesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "AllowedGrantTypesValid", + Status: "True", + LastTransitionTime: time, + Reason: "Success", + Message: `"allowedGrantTypes" is valid`, + ObservedGeneration: observedGeneration, + } + } + + sadAllowedGrantTypesCondition := func(time metav1.Time, observedGeneration int64, message string) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "AllowedGrantTypesValid", + Status: "False", + LastTransitionTime: time, + Reason: "MissingRequiredValue", + Message: message, + ObservedGeneration: observedGeneration, + } + } + + happyClientSecretsCondition := func(howMany int, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "ClientSecretExists", + Status: "True", + LastTransitionTime: time, + Reason: "Success", + Message: fmt.Sprintf(`%d client secret(s) found`, howMany), + ObservedGeneration: observedGeneration, + } + } + + sadClientSecretsCondition := func(time metav1.Time, observedGeneration int64, message string) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "ClientSecretExists", + Status: "False", + LastTransitionTime: time, + Reason: "NoClientSecretFound", + Message: message, + ObservedGeneration: observedGeneration, + } + } + + happyAllowedScopesCondition := func(time metav1.Time, observedGeneration int64) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "AllowedScopesValid", + Status: "True", + LastTransitionTime: time, + Reason: "Success", + Message: `"allowedScopes" is valid`, + ObservedGeneration: observedGeneration, + } + } + + sadAllowedScopesCondition := func(time metav1.Time, observedGeneration int64, message string) configv1alpha1.Condition { + return configv1alpha1.Condition{ + Type: "AllowedScopesValid", + Status: "False", + LastTransitionTime: time, + Reason: "MissingRequiredValue", + Message: message, + ObservedGeneration: observedGeneration, + } + } + + secretNameForUID := func(uid string) string { + // See GetName() in OIDCClientSecretStorage for how the production code determines the Secret name. + // This test helper is intended to choose the same name. + return "pinniped-storage-oidc-client-secret-" + + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString([]byte(uid))) + } + + secretStringDataWithZeroClientSecrets := map[string][]byte{ + "pinniped-storage-data": []byte(`{"version":"1","hashes":[]}`), + "pinniped-storage-version": []byte("1"), + } + + secretStringDataWithOneClientSecret := map[string][]byte{ + "pinniped-storage-data": []byte(`{"version":"1","hashes":["` + testBcryptSecret1 + `"]}`), + "pinniped-storage-version": []byte("1"), + } + + secretStringDataWithTwoClientSecrets := map[string][]byte{ + "pinniped-storage-data": []byte(`{"version":"1","hashes":["` + testBcryptSecret1 + `","` + testBcryptSecret2 + `"]}`), + "pinniped-storage-version": []byte("1"), + } + + secretStringDataWithWrongVersion := map[string][]byte{ + "pinniped-storage-data": []byte(`{"version":"wrong-version","hashes":[]}`), + "pinniped-storage-version": []byte("1"), + } + + storageSecretForUIDWithData := func(uid string, data map[string][]byte) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: testNamespace, + Name: secretNameForUID(uid), + Labels: map[string]string{"storage.pinniped.dev/type": "oidc-client-secret"}, + }, + Type: "storage.pinniped.dev/oidc-client-secret", + Data: data, + } + } + + tests := []struct { + name string + inputObjects []runtime.Object + inputSecrets []runtime.Object + wantErr string + wantResultingOIDCClients []configv1alpha1.OIDCClient + wantAPIActions int + }{ + { + name: "no OIDCClients", + wantAPIActions: 0, // no updates + }, + { + name: "successfully validate minimal OIDCClient and one client secret stored", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate minimal OIDCClient and two client secrets stored", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithTwoClientSecrets)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(2, now, 1234), + }, + }, + }}, + }, + { + name: "an already validated OIDCClient does not have its conditions updated when everything is still valid", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(earlier, 1234), + happyAllowedScopesCondition(earlier, 1234), + happyClientSecretsCondition(1, earlier, 1234), + }, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 0, // no updates + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(earlier, 1234), + happyAllowedScopesCondition(earlier, 1234), + happyClientSecretsCondition(1, earlier, 1234), + }, + }, + }}, + }, + { + name: "missing required minimum settings and missing client secret storage", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{}, + }}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + sadAllowedGrantTypesCondition(now, 1234, `"authorization_code" must always be included in "allowedGrantTypes"`), + sadAllowedScopesCondition(now, 1234, `"openid" must always be included in "allowedScopes"`), + sadClientSecretsCondition(now, 1234, "no client secret found (no Secret storage found)"), + }, + }, + }}, + }, + { + name: "client secret storage exists but cannot be read", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithWrongVersion)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + sadClientSecretsCondition(now, 1234, "error reading client secret storage: OIDC client secret storage data has wrong version: OIDC client secret storage has version wrong-version instead of 1"), + }, + }, + }}, + }, + { + name: "client secret storage exists but does not contain any client secrets", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithZeroClientSecrets)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + sadClientSecretsCondition(now, 1234, "no client secret found (empty list in storage)"), + }, + }, + }}, + }, + { + name: "can operate on multiple at a time, e.g. one is valid one another is missing required minimum settings", + inputObjects: []runtime.Object{ + &configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test1", Generation: 1234, UID: "uid1"}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }, + &configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test2", Generation: 4567, UID: "uid2"}, + Spec: configv1alpha1.OIDCClientSpec{}, + }, + }, + inputSecrets: []runtime.Object{storageSecretForUIDWithData("uid1", secretStringDataWithOneClientSecret)}, + wantAPIActions: 2, // one update for each OIDCClient + wantResultingOIDCClients: []configv1alpha1.OIDCClient{ + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test1", Generation: 1234, UID: "uid1"}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: "test2", Generation: 4567, UID: "uid2"}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + sadAllowedGrantTypesCondition(now, 4567, `"authorization_code" must always be included in "allowedGrantTypes"`), + sadAllowedScopesCondition(now, 4567, `"openid" must always be included in "allowedScopes"`), + sadClientSecretsCondition(now, 4567, "no client secret found (no Secret storage found)"), + }, + }, + }, + }, + }, + { + name: "a previously invalid OIDCClient has its spec changed to become valid so the conditions are updated", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 4567, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + // was invalid on previous run of controller which observed an old generation at an earlier time + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + sadAllowedGrantTypesCondition(earlier, 1234, `"authorization_code" must always be included in "allowedGrantTypes"`), + sadAllowedScopesCondition(earlier, 1234, `"openid" must always be included in "allowedScopes"`), + happyClientSecretsCondition(1, earlier, 1234), + }, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 4567, UID: testUID}, + // status was updated to reflect the current generation at the current time + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 4567), + happyAllowedScopesCondition(now, 4567), + happyClientSecretsCondition(1, earlier, 4567), // was already validated earlier + }, + }, + }}, + }, + { + name: "refresh_token must be included in allowedGrantTypes when offline_access is included in allowedScopes", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access"}, + }, + }}, + wantAPIActions: 1, // one update + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + sadAllowedGrantTypesCondition(now, 1234, `"refresh_token" must be included in "allowedGrantTypes" when "offline_access" is included in "allowedScopes"`), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "urn:ietf:params:oauth:grant-type:token-exchange must be included in allowedGrantTypes when pinniped:request-audience is included in allowedScopes", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience", "username", "groups"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + sadAllowedGrantTypesCondition(now, 1234, `"urn:ietf:params:oauth:grant-type:token-exchange" must be included in "allowedGrantTypes" when "pinniped:request-audience" is included in "allowedScopes"`), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "offline_access must be included in allowedScopes when refresh_token is included in allowedGrantTypes", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + sadAllowedScopesCondition(now, 1234, `"offline_access" must be included in "allowedScopes" when "refresh_token" is included in "allowedGrantTypes"`), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "username and groups must also be included in allowedScopes when pinniped:request-audience is included in allowedScopes: both missing", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + sadAllowedScopesCondition(now, 1234, `"username" and "groups" must be included in "allowedScopes" when "pinniped:request-audience" is included in "allowedScopes"`), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "username and groups must also be included in allowedScopes when pinniped:request-audience is included in allowedScopes: username missing", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience", "groups"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + sadAllowedScopesCondition(now, 1234, `"username" and "groups" must be included in "allowedScopes" when "pinniped:request-audience" is included in "allowedScopes"`), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "username and groups must also be included in allowedScopes when pinniped:request-audience is included in allowedScopes: groups missing", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "pinniped:request-audience", "username"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + sadAllowedScopesCondition(now, 1234, `"username" and "groups" must be included in "allowedScopes" when "pinniped:request-audience" is included in "allowedScopes"`), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "pinniped:request-audience must be included in allowedScopes when urn:ietf:params:oauth:grant-type:token-exchange is included in allowedGrantTypes", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange"}, + AllowedScopes: []configv1alpha1.Scope{"openid"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Error", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + sadAllowedScopesCondition(now, 1234, `"pinniped:request-audience" must be included in "allowedScopes" when "urn:ietf:params:oauth:grant-type:token-exchange" is included in "allowedGrantTypes"`), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient with all allowedGrantTypes and all allowedScopes", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient for offline access without kube API access without username/groups", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient for offline access without kube API access with username", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient for offline access without kube API access with groups", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "groups"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient for offline access without kube API access with both username and groups", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code", "refresh_token"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "offline_access", "username", "groups"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient without offline access without kube API access with username", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "username"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient without offline access without kube API access with groups", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "username"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + { + name: "successfully validate an OIDCClient without offline access without kube API access with both username and groups", + inputObjects: []runtime.Object{&configv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Spec: configv1alpha1.OIDCClientSpec{ + AllowedGrantTypes: []configv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []configv1alpha1.Scope{"openid", "username", "groups"}, + }, + }}, + inputSecrets: []runtime.Object{storageSecretForUIDWithData(testUID, secretStringDataWithOneClientSecret)}, + wantAPIActions: 1, // one update + wantResultingOIDCClients: []configv1alpha1.OIDCClient{{ + ObjectMeta: metav1.ObjectMeta{Namespace: testNamespace, Name: testName, Generation: 1234, UID: testUID}, + Status: configv1alpha1.OIDCClientStatus{ + Phase: "Ready", + Conditions: []configv1alpha1.Condition{ + happyAllowedGrantTypesCondition(now, 1234), + happyAllowedScopesCondition(now, 1234), + happyClientSecretsCondition(1, now, 1234), + }, + }, + }}, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + fakePinnipedClient := pinnipedfake.NewSimpleClientset(tt.inputObjects...) + fakePinnipedClientForInformers := pinnipedfake.NewSimpleClientset(tt.inputObjects...) + pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(fakePinnipedClientForInformers, 0) + fakeKubeClient := kubernetesfake.NewSimpleClientset(tt.inputSecrets...) + kubeInformers := kubeinformers.NewSharedInformerFactoryWithOptions(fakeKubeClient, 0) + + controller := NewOIDCClientWatcherController( + fakePinnipedClient, + kubeInformers.Core().V1().Secrets(), + pinnipedInformers.Config().V1alpha1().OIDCClients(), + controllerlib.WithInformer, + ) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + pinnipedInformers.Start(ctx.Done()) + kubeInformers.Start(ctx.Done()) + controllerlib.TestRunSynchronously(t, controller) + + syncCtx := controllerlib.Context{Context: ctx, Key: controllerlib.Key{}} + + if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != "" { + require.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + + require.Len(t, fakePinnipedClient.Actions(), tt.wantAPIActions) + + actualOIDCClients, err := fakePinnipedClient.ConfigV1alpha1().OIDCClients(testNamespace).List(ctx, metav1.ListOptions{}) + require.NoError(t, err) + + // Assert on the expected Status of the OIDCClients. Preprocess them a bit so that they're easier to assert against. + require.ElementsMatch(t, tt.wantResultingOIDCClients, normalizeOIDCClients(actualOIDCClients.Items, now)) + }) + } +} + +func normalizeOIDCClients(oidcClients []configv1alpha1.OIDCClient, now metav1.Time) []configv1alpha1.OIDCClient { + result := make([]configv1alpha1.OIDCClient, 0, len(oidcClients)) + for _, u := range oidcClients { + normalized := u.DeepCopy() + + // We're only interested in comparing the status, so zero out the spec. + normalized.Spec = configv1alpha1.OIDCClientSpec{} + + // Round down the LastTransitionTime values to `now` if they were just updated. This makes + // it much easier to encode assertions about the expected timestamps. + for i := range normalized.Status.Conditions { + if time.Since(normalized.Status.Conditions[i].LastTransitionTime.Time) < 5*time.Second { + normalized.Status.Conditions[i].LastTransitionTime = now + } + } + result = append(result, *normalized) + } + + return result +} diff --git a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go index 2faff38c..599d7400 100644 --- a/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go +++ b/internal/controller/supervisorconfig/oidcupstreamwatcher/oidc_upstream_watcher.go @@ -410,7 +410,7 @@ func (c *oidcWatcherController) updateStatus(ctx context.Context, upstream *v1al log := c.log.WithValues("namespace", upstream.Namespace, "name", upstream.Name) updated := upstream.DeepCopy() - hadErrorCondition := conditionsutil.Merge(conditions, upstream.Generation, &updated.Status.Conditions, log) + hadErrorCondition := conditionsutil.MergeIDPConditions(conditions, upstream.Generation, &updated.Status.Conditions, log) updated.Status.Phase = v1alpha1.PhaseReady if hadErrorCondition { diff --git a/internal/crud/crud.go b/internal/crud/crud.go index 57e73b2a..29ad6b65 100644 --- a/internal/crud/crud.go +++ b/internal/crud/crud.go @@ -1,4 +1,4 @@ -// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved. +// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 package crud @@ -45,6 +45,7 @@ type Storage interface { Update(ctx context.Context, signature, resourceVersion string, data JSON) (newResourceVersion string, err error) Delete(ctx context.Context, signature string) error DeleteByLabel(ctx context.Context, labelName string, labelValue string) error + GetName(signature string) string } type JSON interface{} // document that we need valid JSON types @@ -80,7 +81,7 @@ func (s *secretsStorage) Create(ctx context.Context, signature string, data JSON } func (s *secretsStorage) Get(ctx context.Context, signature string, data JSON) (string, error) { - secret, err := s.secrets.Get(ctx, s.getName(signature), metav1.GetOptions{}) + secret, err := s.secrets.Get(ctx, s.GetName(signature), metav1.GetOptions{}) if err != nil { return "", fmt.Errorf("failed to get %s for signature %s: %w", s.resource, signature, err) } @@ -109,7 +110,7 @@ func (s *secretsStorage) Update(ctx context.Context, signature, resourceVersion } func (s *secretsStorage) Delete(ctx context.Context, signature string) error { - if err := s.secrets.Delete(ctx, s.getName(signature), metav1.DeleteOptions{}); err != nil { + if err := s.secrets.Delete(ctx, s.GetName(signature), metav1.DeleteOptions{}); err != nil { return fmt.Errorf("failed to delete %s for signature %s: %w", s.resource, signature, err) } return nil @@ -171,7 +172,7 @@ func validateSecret(resource string, secret *corev1.Secret) error { //nolint: gochecknoglobals var b32 = base32.StdEncoding.WithPadding(base32.NoPadding) -func (s *secretsStorage) getName(signature string) string { +func (s *secretsStorage) GetName(signature string) string { // try to decode base64 signatures to prevent double encoding of binary data signatureBytes := maybeBase64Decode(signature) // lower case base32 encoding insures that our secret name is valid per ValidateSecretName in k/k @@ -182,7 +183,7 @@ func (s *secretsStorage) getName(signature string) string { func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, additionalLabels map[string]string) (*corev1.Secret, error) { buf, err := json.Marshal(data) if err != nil { - return nil, fmt.Errorf("failed to encode secret data for %s: %w", s.getName(signature), err) + return nil, fmt.Errorf("failed to encode secret data for %s: %w", s.GetName(signature), err) } labelsToAdd := map[string]string{ @@ -194,7 +195,7 @@ func (s *secretsStorage) toSecret(signature, resourceVersion string, data JSON, return &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: s.getName(signature), + Name: s.GetName(signature), ResourceVersion: resourceVersion, Labels: labelsToAdd, Annotations: map[string]string{ diff --git a/internal/oidcclientsecretstorage/oidcclientsecretstorage.go b/internal/oidcclientsecretstorage/oidcclientsecretstorage.go new file mode 100644 index 00000000..257e674c --- /dev/null +++ b/internal/oidcclientsecretstorage/oidcclientsecretstorage.go @@ -0,0 +1,67 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidcclientsecretstorage + +import ( + "encoding/base64" + "fmt" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" + + "go.pinniped.dev/internal/constable" + "go.pinniped.dev/internal/crud" +) + +const ( + TypeLabelValue = "oidc-client-secret" + + ErrOIDCClientSecretStorageVersion = constable.Error("OIDC client secret storage data has wrong version") + + oidcClientSecretStorageVersion = "1" +) + +type OIDCClientSecretStorage struct { + storage crud.Storage +} + +// StoredClientSecret defines the format of the content of a client's secrets when stored in a Secret +// as a JSON string value. +type StoredClientSecret struct { + // List of bcrypt hashes. + SecretHashes []string `json:"hashes"` + // The format version. Take care when updating. We cannot simply bump the storage version and drop/ignore old data. + // Updating this would require some form of migration of existing stored data. + Version string `json:"version"` +} + +func New(secrets corev1client.SecretInterface, clock func() time.Time) *OIDCClientSecretStorage { + // TODO make lifetime = 0 mean that it does not get annotated with any garbage collection annotation + return &OIDCClientSecretStorage{storage: crud.New(TypeLabelValue, secrets, clock, 0)} +} + +// TODO expose other methods as needed for get, create, update, etc. + +// GetName returns the name of the Secret which would be used to store data for the given signature. +func (s *OIDCClientSecretStorage) GetName(oidcClientUID types.UID) string { + // Avoid having s.storage.GetName() base64 decode something that wasn't ever encoded by encoding it here. + b64encodedUID := base64.RawURLEncoding.EncodeToString([]byte(oidcClientUID)) + return s.storage.GetName(b64encodedUID) +} + +// ReadFromSecret reads the contents of a Secret as a StoredClientSecret. +func ReadFromSecret(secret *v1.Secret) (*StoredClientSecret, error) { + storedClientSecret := &StoredClientSecret{} + err := crud.FromSecret(TypeLabelValue, secret, storedClientSecret) + if err != nil { + return nil, err + } + if storedClientSecret.Version != oidcClientSecretStorageVersion { + return nil, fmt.Errorf("%w: OIDC client secret storage has version %s instead of %s", + ErrOIDCClientSecretStorageVersion, storedClientSecret.Version, oidcClientSecretStorageVersion) + } + return storedClientSecret, nil +} diff --git a/internal/oidcclientsecretstorage/oidcclientsecretstorage_test.go b/internal/oidcclientsecretstorage/oidcclientsecretstorage_test.go new file mode 100644 index 00000000..ac81565a --- /dev/null +++ b/internal/oidcclientsecretstorage/oidcclientsecretstorage_test.go @@ -0,0 +1,125 @@ +// Copyright 2022 the Pinniped contributors. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package oidcclientsecretstorage + +import ( + "testing" + + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestGetName(t *testing.T) { + // Note that GetName() should not depend on the constructor params, to make it easier to use in various contexts. + subject := New(nil, nil) + + require.Equal(t, + "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqyq", + subject.GetName("some-example-uid1")) + + require.Equal(t, + "pinniped-storage-oidc-client-secret-onxw2zjnmv4gc3lqnrss25ljmqza", + subject.GetName("some-example-uid2")) +} + +func TestReadFromSecret(t *testing.T) { + tests := []struct { + name string + secret *corev1.Secret + wantStored *StoredClientSecret + wantErr string + }{ + { + name: "happy path", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4", + ResourceVersion: "", + Labels: map[string]string{ + "storage.pinniped.dev/type": "oidc-client-secret", + }, + }, + Data: map[string][]byte{ + "pinniped-storage-data": []byte(`{"hashes":["first-hash","second-hash"],"version":"1"}`), + "pinniped-storage-version": []byte("1"), + }, + Type: "storage.pinniped.dev/oidc-client-secret", + }, + wantStored: &StoredClientSecret{ + Version: "1", + SecretHashes: []string{"first-hash", "second-hash"}, + }, + }, + { + name: "wrong secret type", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4", + ResourceVersion: "", + Labels: map[string]string{ + "storage.pinniped.dev/type": "oidc-client-secret", + }, + }, + Data: map[string][]byte{ + "pinniped-storage-data": []byte(`{"hashes":["first-hash","second-hash"],"version":"1"}`), + "pinniped-storage-version": []byte("1"), + }, + Type: "storage.pinniped.dev/not-oidc-client-secret", + }, + wantErr: "secret storage data has incorrect type: storage.pinniped.dev/not-oidc-client-secret must equal storage.pinniped.dev/oidc-client-secret", + }, + { + name: "wrong stored StoredClientSecret version", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4", + ResourceVersion: "", + Labels: map[string]string{ + "storage.pinniped.dev/type": "oidc-client-secret", + }, + }, + Data: map[string][]byte{ + "pinniped-storage-data": []byte(`{"hashes":["first-hash","second-hash"],"version":"wrong-version-here"}`), + "pinniped-storage-version": []byte("1"), + }, + Type: "storage.pinniped.dev/oidc-client-secret", + }, + wantErr: "OIDC client secret storage data has wrong version: OIDC client secret storage has version wrong-version-here instead of 1", + }, + { + name: "wrong storage version", + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pinniped-storage-oidc-client-secret-pwu5zs7lekbhnln2w4", + ResourceVersion: "", + Labels: map[string]string{ + "storage.pinniped.dev/type": "oidc-client-secret", + }, + }, + Data: map[string][]byte{ + "pinniped-storage-data": []byte(`{"hashes":["first-hash","second-hash"],"version":"1"}`), + "pinniped-storage-version": []byte("wrong-version-here"), + }, + Type: "storage.pinniped.dev/oidc-client-secret", + }, + wantErr: "secret storage data has incorrect version", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + session, err := ReadFromSecret(tt.secret) + if tt.wantErr == "" { + require.NoError(t, err) + require.Equal(t, tt.wantStored, session) + } else { + require.EqualError(t, err, tt.wantErr) + require.Nil(t, session) + } + }) + } +} diff --git a/internal/supervisor/server/server.go b/internal/supervisor/server/server.go index 30cdf48f..677165ee 100644 --- a/internal/supervisor/server/server.go +++ b/internal/supervisor/server/server.go @@ -46,6 +46,7 @@ import ( "go.pinniped.dev/internal/controller/supervisorconfig/activedirectoryupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/generator" "go.pinniped.dev/internal/controller/supervisorconfig/ldapupstreamwatcher" + "go.pinniped.dev/internal/controller/supervisorconfig/oidcclientwatcher" "go.pinniped.dev/internal/controller/supervisorconfig/oidcupstreamwatcher" "go.pinniped.dev/internal/controller/supervisorstorage" "go.pinniped.dev/internal/controllerinit" @@ -141,6 +142,7 @@ func prepareControllers( const certificateName string = "pinniped-supervisor-api-tls-serving-certificate" clientSecretSupervisorGroupData := groupsuffix.SupervisorAggregatedGroups(*cfg.APIGroupSuffix) federationDomainInformer := pinnipedInformers.Config().V1alpha1().FederationDomains() + oidcClientInformer := pinnipedInformers.Config().V1alpha1().OIDCClients() secretInformer := kubeInformers.Core().V1().Secrets() // Create controller manager. @@ -356,6 +358,15 @@ func prepareControllers( plog.New(), ), singletonWorker, + ). + WithController( + oidcclientwatcher.NewOIDCClientWatcherController( + pinnipedClient, + secretInformer, + oidcClientInformer, + controllerlib.WithInformer, + ), + singletonWorker, ) return controllerinit.Prepare(controllerManager.Start, leaderElector, kubeInformers, pinnipedInformers) diff --git a/test/integration/oidc_client_test.go b/test/integration/supervisor_oidc_client_test.go similarity index 66% rename from test/integration/oidc_client_test.go rename to test/integration/supervisor_oidc_client_test.go index fe77b3b8..adb43403 100644 --- a/test/integration/oidc_client_test.go +++ b/test/integration/supervisor_oidc_client_test.go @@ -17,6 +17,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1" + "go.pinniped.dev/internal/oidcclientsecretstorage" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/test/testlib" ) @@ -464,3 +465,205 @@ func makeErrFix(reallyOld bool) []string { return out } + +func TestOIDCClientControllerValidations_Parallel(t *testing.T) { + env := testlib.IntegrationEnv(t) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + t.Cleanup(cancel) + + secrets := testlib.NewKubernetesClientset(t).CoreV1().Secrets(env.SupervisorNamespace) + oidcClients := testlib.NewSupervisorClientset(t).ConfigV1alpha1().OIDCClients(env.SupervisorNamespace) + + tests := []struct { + name string + client *supervisorconfigv1alpha1.OIDCClient + secret *corev1.Secret + wantPhase string + wantConditions []supervisorconfigv1alpha1.Condition + }{ + { + name: "invalid AllowedGrantTypes and AllowedScopes (missing minimum required values), with no Secret", + client: &supervisorconfigv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "client.oauth.pinniped.dev-", + }, + Spec: supervisorconfigv1alpha1.OIDCClientSpec{ + AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{"https://some-redirect-url.test.pinniped.dev/some/path"}, + AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{"refresh_token"}, // needs to have authorization_code + AllowedScopes: []supervisorconfigv1alpha1.Scope{"username"}, // needs to have openid + }, + }, + wantPhase: "Error", + wantConditions: []supervisorconfigv1alpha1.Condition{ + { + Type: "AllowedGrantTypesValid", + Status: "False", + Reason: "MissingRequiredValue", + Message: `"authorization_code" must always be included in "allowedGrantTypes"`, + }, + { + Type: "AllowedScopesValid", + Status: "False", + Reason: "MissingRequiredValue", + Message: `"openid" must always be included in "allowedScopes"`, + }, + { + Type: "ClientSecretExists", + Status: "False", + Reason: "NoClientSecretFound", + Message: `no client secret found (no Secret storage found)`, + }, + }, + }, + { + name: "minimal valid AllowedGrantTypes and AllowedScopes, with Secret that contains empty list of client secrets", + client: &supervisorconfigv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "client.oauth.pinniped.dev-", + }, + Spec: supervisorconfigv1alpha1.OIDCClientSpec{ + AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{"https://some-redirect-url.test.pinniped.dev/some/path"}, + AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{"authorization_code"}, + AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid"}, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"storage.pinniped.dev/type": "oidc-client-secret"}, + }, + Type: "storage.pinniped.dev/oidc-client-secret", + Data: map[string][]byte{ + "pinniped-storage-data": []byte(`{"version":"1","hashes":[]}`), + "pinniped-storage-version": []byte("1"), + }, + }, + wantPhase: "Error", + wantConditions: []supervisorconfigv1alpha1.Condition{ + { + Type: "AllowedGrantTypesValid", + Status: "True", + Reason: "Success", + Message: `"allowedGrantTypes" is valid`, + }, + { + Type: "AllowedScopesValid", + Status: "True", + Reason: "Success", + Message: `"allowedScopes" is valid`, + }, + { + Type: "ClientSecretExists", + Status: "False", + Reason: "NoClientSecretFound", + Message: `no client secret found (empty list in storage)`, + }, + }, + }, + { + name: "happy path example with one client secret stored and all possible AllowedGrantTypes and AllowedScopes", + client: &supervisorconfigv1alpha1.OIDCClient{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "client.oauth.pinniped.dev-", + }, + Spec: supervisorconfigv1alpha1.OIDCClientSpec{ + AllowedRedirectURIs: []supervisorconfigv1alpha1.RedirectURI{"https://some-redirect-url.test.pinniped.dev/some/path"}, + AllowedGrantTypes: []supervisorconfigv1alpha1.GrantType{"authorization_code", "urn:ietf:params:oauth:grant-type:token-exchange", "refresh_token"}, + AllowedScopes: []supervisorconfigv1alpha1.Scope{"openid", "offline_access", "pinniped:request-audience", "username", "groups"}, + }, + }, + secret: &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"storage.pinniped.dev/type": "oidc-client-secret"}, + }, + Type: "storage.pinniped.dev/oidc-client-secret", + Data: map[string][]byte{ + "pinniped-storage-data": []byte(`{"version":"1","hashes":["$2y$15$Kh7cRj0ScSD5QelE3ZNSl.nF04JDv7zb3SgGN.tSfLIX.4kt3UX7m"]}`), + "pinniped-storage-version": []byte("1"), + }, + }, + wantPhase: "Ready", + wantConditions: []supervisorconfigv1alpha1.Condition{ + { + Type: "AllowedGrantTypesValid", + Status: "True", + Reason: "Success", + Message: `"allowedGrantTypes" is valid`, + }, + { + Type: "AllowedScopesValid", + Status: "True", + Reason: "Success", + Message: `"allowedScopes" is valid`, + }, + { + Type: "ClientSecretExists", + Status: "True", + Reason: "Success", + Message: `1 client secret(s) found`, + }, + }, + }, + // Note: there are many more possible combinations of these settings, but they are covered by the controller's + // unit tests. This test ensures that everything is wired up correctly in regard to this controller, enough to + // allow the controller to work correctly. + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + client, err := oidcClients.Create(ctx, tt.client, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + t.Logf("cleaning up test OIDCClient %s/%s", client.Namespace, client.Name) + err := oidcClients.Delete(ctx, client.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + + if tt.secret != nil { + // Force the Secret's name to match the client created above. + tt.secret.Name = oidcclientsecretstorage.New(nil, nil).GetName(client.UID) + secret, err := secrets.Create(ctx, tt.secret, metav1.CreateOptions{}) + require.NoError(t, err) + t.Cleanup(func() { + t.Logf("cleaning up test Secret %s/%s", secret.Namespace, secret.Name) + err := secrets.Delete(ctx, secret.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + } + + // Wait for the OIDCClient to enter the expected phase (or time out). + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + var err error + updatedClient, err := oidcClients.Get(ctx, client.Name, metav1.GetOptions{}) + requireEventually.NoErrorf(err, "error while getting OIDCClient %s/%s", client.Namespace, client.Name) + requireEventually.Equalf(supervisorconfigv1alpha1.OIDCClientPhase(tt.wantPhase), updatedClient.Status.Phase, + "OIDCClient is not in phase %s: %v", tt.wantPhase, testlib.Sdump(updatedClient)) + }, 1*time.Minute, 2*time.Second, "expected the OIDCClient to go into phase %s", tt.wantPhase) + + // Wait for the controller to converge to the expected Conditions list. It may take several passes of the + // controller running, since the Secret is created after the OIDCClient is created, potentially causing + // the controller to Sync at least twice. + testlib.RequireEventuallyf(t, func(requireEventually *require.Assertions) { + var err error + updatedClient, err := oidcClients.Get(ctx, client.Name, metav1.GetOptions{}) + requireEventually.NoErrorf(err, "error while getting OIDCClient %s/%s", client.Namespace, client.Name) + + // Note that the controller sorts the conditions by type name, + // so we can assume that ordering in the test expectations for this test. + requireEventually.Len(updatedClient.Status.Conditions, len(tt.wantConditions)) + for i, want := range tt.wantConditions { + actual := updatedClient.Status.Conditions[i] + requireEventually.Equal(want.Type, actual.Type) + requireEventually.Equal(want.Status, actual.Status) + requireEventually.Equal(want.Reason, actual.Reason) + requireEventually.Equal(want.Message, actual.Message) + requireEventually.Equal(updatedClient.Generation, actual.ObservedGeneration) + requireEventually.NotEmpty(actual.LastTransitionTime) + } + }, 1*time.Minute, 2*time.Second, "expected the OIDCClient to to have conditions %v", tt.wantConditions) + }) + } +}