2021-04-10 01:49:43 +00:00
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package upstreamwatcher
import (
"context"
2021-04-14 00:16:57 +00:00
"encoding/base64"
2021-04-15 21:44:43 +00:00
"errors"
2021-04-12 20:53:21 +00:00
"fmt"
2021-04-12 21:12:51 +00:00
"sort"
2021-04-10 01:49:43 +00:00
"testing"
2021-04-12 20:53:21 +00:00
"time"
2021-04-10 01:49:43 +00:00
2021-04-16 21:04:05 +00:00
"github.com/go-ldap/ldap/v3"
2021-04-15 21:44:43 +00:00
"github.com/golang/mock/gomock"
2021-04-10 01:49:43 +00:00
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
"go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
pinnipedfake "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/fake"
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
2021-04-14 00:16:57 +00:00
"go.pinniped.dev/internal/certauthority"
2021-04-10 01:49:43 +00:00
"go.pinniped.dev/internal/controllerlib"
2021-04-15 21:44:43 +00:00
"go.pinniped.dev/internal/mocks/mockldapconn"
2021-04-10 01:49:43 +00:00
"go.pinniped.dev/internal/oidc/provider"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/upstreamldap"
)
func TestLDAPUpstreamWatcherControllerFilterSecrets ( 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 : corev1 . SecretTypeBasicAuth ,
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 : "this-is-the-wrong-type" ,
ObjectMeta : metav1 . ObjectMeta { Name : "some-name" , Namespace : "some-namespace" } ,
} ,
} ,
{
name : "resource of a data type which is not watched by this controller" ,
secret : & corev1 . Namespace {
ObjectMeta : metav1 . ObjectMeta { Name : "some-name" , Namespace : "some-namespace" } ,
} ,
} ,
}
for _ , test := range tests {
test := test
t . Run ( test . name , func ( t * testing . T ) {
t . Parallel ( )
fakePinnipedClient := pinnipedfake . NewSimpleClientset ( )
pinnipedInformers := pinnipedinformers . NewSharedInformerFactory ( fakePinnipedClient , 0 )
ldapIDPInformer := pinnipedInformers . IDP ( ) . V1alpha1 ( ) . LDAPIdentityProviders ( )
fakeKubeClient := fake . NewSimpleClientset ( )
kubeInformers := informers . NewSharedInformerFactory ( fakeKubeClient , 0 )
secretInformer := kubeInformers . Core ( ) . V1 ( ) . Secrets ( )
withInformer := testutil . NewObservableWithInformerOption ( )
2021-04-27 19:43:09 +00:00
NewLDAPUpstreamWatcherController ( nil , nil , ldapIDPInformer , secretInformer , withInformer . WithInformer )
2021-04-10 01:49:43 +00:00
unrelated := corev1 . Secret { }
filter := withInformer . GetFilterForInformer ( secretInformer )
require . Equal ( t , test . wantAdd , filter . Add ( test . secret ) )
require . Equal ( t , test . wantUpdate , filter . Update ( & unrelated , test . secret ) )
require . Equal ( t , test . wantUpdate , filter . Update ( test . secret , & unrelated ) )
require . Equal ( t , test . wantDelete , filter . Delete ( test . secret ) )
} )
}
}
func TestLDAPUpstreamWatcherControllerFilterLDAPIdentityProviders ( t * testing . T ) {
t . Parallel ( )
tests := [ ] struct {
name string
idp metav1 . Object
wantAdd bool
wantUpdate bool
wantDelete bool
} {
{
name : "any LDAPIdentityProvider" ,
idp : & v1alpha1 . LDAPIdentityProvider {
ObjectMeta : metav1 . ObjectMeta { Name : "some-name" , Namespace : "some-namespace" } ,
} ,
wantAdd : true ,
wantUpdate : true ,
wantDelete : true ,
} ,
}
for _ , test := range tests {
test := test
t . Run ( test . name , func ( t * testing . T ) {
t . Parallel ( )
fakePinnipedClient := pinnipedfake . NewSimpleClientset ( )
pinnipedInformers := pinnipedinformers . NewSharedInformerFactory ( fakePinnipedClient , 0 )
ldapIDPInformer := pinnipedInformers . IDP ( ) . V1alpha1 ( ) . LDAPIdentityProviders ( )
fakeKubeClient := fake . NewSimpleClientset ( )
kubeInformers := informers . NewSharedInformerFactory ( fakeKubeClient , 0 )
secretInformer := kubeInformers . Core ( ) . V1 ( ) . Secrets ( )
withInformer := testutil . NewObservableWithInformerOption ( )
2021-04-27 19:43:09 +00:00
NewLDAPUpstreamWatcherController ( nil , nil , ldapIDPInformer , secretInformer , withInformer . WithInformer )
2021-04-10 01:49:43 +00:00
unrelated := corev1 . Secret { }
filter := withInformer . GetFilterForInformer ( ldapIDPInformer )
require . Equal ( t , test . wantAdd , filter . Add ( test . idp ) )
require . Equal ( t , test . wantUpdate , filter . Update ( & unrelated , test . idp ) )
require . Equal ( t , test . wantUpdate , filter . Update ( test . idp , & unrelated ) )
require . Equal ( t , test . wantDelete , filter . Delete ( test . idp ) )
} )
}
}
2021-04-12 18:23:08 +00:00
// Wrap the func into a struct so the test can do deep equal assertions on instances of upstreamldap.Provider.
type comparableDialer struct {
f upstreamldap . LDAPDialerFunc
}
func ( d * comparableDialer ) Dial ( ctx context . Context , hostAndPort string ) ( upstreamldap . Conn , error ) {
return d . f ( ctx , hostAndPort )
}
2021-04-10 01:49:43 +00:00
func TestLDAPUpstreamWatcherControllerSync ( t * testing . T ) {
t . Parallel ( )
2021-04-12 20:53:21 +00:00
now := metav1 . NewTime ( time . Now ( ) . UTC ( ) )
2021-04-10 01:49:43 +00:00
2021-04-12 18:23:08 +00:00
const (
testNamespace = "test-namespace"
testName = "test-name"
2021-04-12 20:53:21 +00:00
testSecretName = "test-bind-secret"
2021-04-12 18:23:08 +00:00
testBindUsername = "test-bind-username"
testBindPassword = "test-bind-password"
testHost = "ldap.example.com:123"
testUserSearchBase = "test-user-search-base"
testUserSearchFilter = "test-user-search-filter"
testUsernameAttrName = "test-username-attr"
testUIDAttrName = "test-uid-attr"
)
2021-04-14 00:16:57 +00:00
testValidSecretData := map [ string ] [ ] byte { "username" : [ ] byte ( testBindUsername ) , "password" : [ ] byte ( testBindPassword ) }
testCA , err := certauthority . New ( "test CA" , time . Minute )
require . NoError ( t , err )
testCABundle := testCA . Bundle ( )
testCABundleBase64Encoded := base64 . StdEncoding . EncodeToString ( testCABundle )
2021-04-12 18:23:08 +00:00
2021-04-12 20:53:21 +00:00
validUpstream := & v1alpha1 . LDAPIdentityProvider {
ObjectMeta : metav1 . ObjectMeta { Name : testName , Namespace : testNamespace , Generation : 1234 } ,
Spec : v1alpha1 . LDAPIdentityProviderSpec {
Host : testHost ,
2021-04-27 19:43:09 +00:00
TLS : & v1alpha1 . TLSSpec { CertificateAuthorityData : testCABundleBase64Encoded } ,
Bind : v1alpha1 . LDAPIdentityProviderBind { SecretName : testSecretName } ,
UserSearch : v1alpha1 . LDAPIdentityProviderUserSearch {
2021-04-12 20:53:21 +00:00
Base : testUserSearchBase ,
Filter : testUserSearchFilter ,
2021-04-27 19:43:09 +00:00
Attributes : v1alpha1 . LDAPIdentityProviderUserSearchAttributes {
2021-04-12 20:53:21 +00:00
Username : testUsernameAttrName ,
2021-04-27 19:43:09 +00:00
UID : testUIDAttrName ,
2021-04-12 20:53:21 +00:00
} ,
} ,
} ,
}
2021-04-15 23:46:27 +00:00
editedValidUpstream := func ( editFunc func ( * v1alpha1 . LDAPIdentityProvider ) ) * v1alpha1 . LDAPIdentityProvider {
2021-04-12 21:12:51 +00:00
deepCopy := validUpstream . DeepCopy ( )
editFunc ( deepCopy )
return deepCopy
}
2021-04-12 20:53:21 +00:00
2021-04-15 17:25:35 +00:00
providerConfigForValidUpstream := & upstreamldap . ProviderConfig {
2021-04-12 20:53:21 +00:00
Name : testName ,
Host : testHost ,
2021-04-14 00:16:57 +00:00
CABundle : testCABundle ,
2021-04-12 20:53:21 +00:00
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
2021-04-15 17:25:35 +00:00
UserSearch : upstreamldap . UserSearchConfig {
2021-04-12 20:53:21 +00:00
Base : testUserSearchBase ,
Filter : testUserSearchFilter ,
UsernameAttribute : testUsernameAttrName ,
UIDAttribute : testUIDAttrName ,
} ,
}
2021-04-15 23:46:27 +00:00
bindSecretValidTrueCondition := func ( gen int64 ) v1alpha1 . Condition {
return v1alpha1 . Condition {
Type : "BindSecretValid" ,
Status : "True" ,
LastTransitionTime : now ,
Reason : "Success" ,
Message : "loaded bind secret" ,
ObservedGeneration : gen ,
}
}
2021-04-16 00:45:15 +00:00
ldapConnectionValidTrueCondition := func ( gen int64 , secretVersion string ) v1alpha1 . Condition {
2021-04-15 23:46:27 +00:00
return v1alpha1 . Condition {
Type : "LDAPConnectionValid" ,
Status : "True" ,
LastTransitionTime : now ,
Reason : "Success" ,
2021-04-16 00:45:15 +00:00
Message : fmt . Sprintf (
` successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"] ` ,
testHost , testBindUsername , testSecretName , secretVersion ) ,
2021-04-15 23:46:27 +00:00
ObservedGeneration : gen ,
}
}
tlsConfigurationValidLoadedTrueCondition := func ( gen int64 ) v1alpha1 . Condition {
return v1alpha1 . Condition {
Type : "TLSConfigurationValid" ,
Status : "True" ,
LastTransitionTime : now ,
Reason : "Success" ,
Message : "loaded TLS configuration" ,
ObservedGeneration : gen ,
}
}
2021-04-16 00:45:15 +00:00
allConditionsTrue := func ( gen int64 , secretVersion string ) [ ] v1alpha1 . Condition {
2021-04-15 23:46:27 +00:00
return [ ] v1alpha1 . Condition {
bindSecretValidTrueCondition ( gen ) ,
2021-04-16 00:45:15 +00:00
ldapConnectionValidTrueCondition ( gen , secretVersion ) ,
2021-04-15 23:46:27 +00:00
tlsConfigurationValidLoadedTrueCondition ( gen ) ,
}
}
2021-04-16 00:45:15 +00:00
validBindUserSecret := func ( secretVersion string ) * corev1 . Secret {
return & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta { Name : testSecretName , Namespace : testNamespace , ResourceVersion : secretVersion } ,
Type : corev1 . SecretTypeBasicAuth ,
Data : testValidSecretData ,
}
}
2021-04-10 01:49:43 +00:00
tests := [ ] struct {
name string
inputUpstreams [ ] runtime . Object
inputSecrets [ ] runtime . Object
2021-04-15 21:44:43 +00:00
setupMocks func ( conn * mockldapconn . MockConn )
dialError error
2021-04-10 01:49:43 +00:00
wantErr string
2021-04-15 17:25:35 +00:00
wantResultingCache [ ] * upstreamldap . ProviderConfig
2021-04-10 01:49:43 +00:00
wantResultingUpstreams [ ] v1alpha1 . LDAPIdentityProvider
} {
{
2021-04-15 21:44:43 +00:00
name : "no LDAPIdentityProvider upstreams clears the cache" ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
2021-04-10 01:49:43 +00:00
} ,
{
2021-04-12 20:53:21 +00:00
name : "one valid upstream updates the cache to include only that upstream" ,
inputUpstreams : [ ] runtime . Object { validUpstream } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
2021-04-15 21:44:43 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
2021-04-12 20:53:21 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
2021-04-15 23:46:27 +00:00
Phase : "Ready" ,
2021-04-16 00:45:15 +00:00
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
2021-04-12 20:53:21 +00:00
} ,
} } ,
} ,
{
name : "missing secret" ,
inputUpstreams : [ ] runtime . Object { validUpstream } ,
inputSecrets : [ ] runtime . Object { } ,
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
2021-04-12 20:53:21 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
{
Type : "BindSecretValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "SecretNotFound" ,
Message : fmt . Sprintf ( ` secret "%s" not found ` , testSecretName ) ,
ObservedGeneration : 1234 ,
2021-04-10 01:49:43 +00:00
} ,
2021-04-15 23:46:27 +00:00
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
2021-04-10 01:49:43 +00:00
} ,
} ,
} } ,
2021-04-12 20:53:21 +00:00
} ,
{
name : "secret has wrong type" ,
inputUpstreams : [ ] runtime . Object { validUpstream } ,
2021-04-10 01:49:43 +00:00
inputSecrets : [ ] runtime . Object { & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta { Name : testSecretName , Namespace : testNamespace } ,
2021-04-12 20:53:21 +00:00
Type : "some-other-type" ,
2021-04-10 01:49:43 +00:00
Data : testValidSecretData ,
} } ,
2021-04-12 20:53:21 +00:00
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
2021-04-12 20:53:21 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
{
Type : "BindSecretValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "SecretWrongType" ,
Message : fmt . Sprintf ( ` referenced Secret "%s" has wrong type "some-other-type" (should be "kubernetes.io/basic-auth") ` , testSecretName ) ,
ObservedGeneration : 1234 ,
} ,
2021-04-15 23:46:27 +00:00
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
2021-04-12 18:23:08 +00:00
} ,
2021-04-10 01:49:43 +00:00
} ,
2021-04-12 20:53:21 +00:00
} } ,
} ,
{
name : "secret is missing key" ,
inputUpstreams : [ ] runtime . Object { validUpstream } ,
inputSecrets : [ ] runtime . Object { & corev1 . Secret {
ObjectMeta : metav1 . ObjectMeta { Name : testSecretName , Namespace : testNamespace } ,
Type : corev1 . SecretTypeBasicAuth ,
} } ,
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
2021-04-10 01:49:43 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
2021-04-12 20:53:21 +00:00
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
{
Type : "BindSecretValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "SecretMissingKeys" ,
Message : fmt . Sprintf ( ` referenced Secret "%s" is missing required keys ["username" "password"] ` , testSecretName ) ,
ObservedGeneration : 1234 ,
} ,
2021-04-15 23:46:27 +00:00
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
2021-04-14 00:16:57 +00:00
} ,
} ,
} } ,
} ,
{
2021-04-15 21:44:43 +00:00
name : "CertificateAuthorityData is not base64 encoded" ,
2021-04-15 23:46:27 +00:00
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
2021-04-14 00:16:57 +00:00
upstream . Spec . TLS . CertificateAuthorityData = "this-is-not-base64-encoded"
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "" ) } ,
2021-04-14 00:16:57 +00:00
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
2021-04-14 00:16:57 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
2021-04-15 23:46:27 +00:00
bindSecretValidTrueCondition ( 1234 ) ,
2021-04-14 00:16:57 +00:00
{
Type : "TLSConfigurationValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "InvalidTLSConfig" ,
Message : "certificateAuthorityData is invalid: illegal base64 data at input byte 4" ,
ObservedGeneration : 1234 ,
} ,
} ,
} ,
} } ,
} ,
{
2021-04-15 21:44:43 +00:00
name : "CertificateAuthorityData is not valid pem data" ,
2021-04-15 23:46:27 +00:00
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
2021-04-14 00:16:57 +00:00
upstream . Spec . TLS . CertificateAuthorityData = base64 . StdEncoding . EncodeToString ( [ ] byte ( "this is not pem data" ) )
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "" ) } ,
2021-04-14 00:16:57 +00:00
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
2021-04-14 00:16:57 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
2021-04-15 23:46:27 +00:00
bindSecretValidTrueCondition ( 1234 ) ,
2021-04-14 00:16:57 +00:00
{
Type : "TLSConfigurationValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "InvalidTLSConfig" ,
Message : "certificateAuthorityData is invalid: no certificates found" ,
ObservedGeneration : 1234 ,
} ,
} ,
} ,
} } ,
} ,
{
2021-04-15 21:44:43 +00:00
name : "nil TLS configuration is valid" ,
2021-04-15 23:46:27 +00:00
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
2021-04-14 00:16:57 +00:00
upstream . Spec . TLS = nil
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
2021-04-15 21:44:43 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig {
2021-04-14 00:16:57 +00:00
{
Name : testName ,
Host : testHost ,
CABundle : nil ,
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
2021-04-15 17:25:35 +00:00
UserSearch : upstreamldap . UserSearchConfig {
2021-04-14 00:16:57 +00:00
Base : testUserSearchBase ,
Filter : testUserSearchFilter ,
UsernameAttribute : testUsernameAttrName ,
UIDAttribute : testUIDAttrName ,
} ,
} ,
} ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Ready" ,
Conditions : [ ] v1alpha1 . Condition {
2021-04-15 23:46:27 +00:00
bindSecretValidTrueCondition ( 1234 ) ,
2021-04-16 00:45:15 +00:00
ldapConnectionValidTrueCondition ( 1234 , "4242" ) ,
2021-04-14 00:16:57 +00:00
{
Type : "TLSConfigurationValid" ,
Status : "True" ,
LastTransitionTime : now ,
Reason : "Success" ,
Message : "no TLS configuration provided" ,
ObservedGeneration : 1234 ,
} ,
} ,
} ,
} } ,
} ,
{
2021-04-15 21:44:43 +00:00
name : "non-nil TLS configuration with empty CertificateAuthorityData is valid" ,
2021-04-15 23:46:27 +00:00
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
2021-04-14 00:16:57 +00:00
upstream . Spec . TLS . CertificateAuthorityData = ""
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
2021-04-15 21:44:43 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig {
2021-04-14 00:16:57 +00:00
{
Name : testName ,
Host : testHost ,
CABundle : nil ,
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
2021-04-15 17:25:35 +00:00
UserSearch : upstreamldap . UserSearchConfig {
2021-04-14 00:16:57 +00:00
Base : testUserSearchBase ,
Filter : testUserSearchFilter ,
UsernameAttribute : testUsernameAttrName ,
UIDAttribute : testUIDAttrName ,
} ,
} ,
} ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
2021-04-15 23:46:27 +00:00
Phase : "Ready" ,
2021-04-16 00:45:15 +00:00
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
2021-04-10 01:49:43 +00:00
} ,
} } ,
} ,
2021-04-12 21:12:51 +00:00
{
2021-04-15 21:44:43 +00:00
name : "one valid upstream and one invalid upstream updates the cache to include only the valid upstream" ,
2021-04-15 23:46:27 +00:00
inputUpstreams : [ ] runtime . Object { validUpstream , editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
2021-04-12 21:12:51 +00:00
upstream . Name = "other-upstream"
upstream . Generation = 42
upstream . Spec . Bind . SecretName = "non-existent-secret"
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
2021-04-15 21:44:43 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind for the one valid upstream configuration.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-12 21:12:51 +00:00
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
2021-04-15 17:25:35 +00:00
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
2021-04-12 21:12:51 +00:00
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider {
{
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : "other-upstream" , Generation : 42 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
{
Type : "BindSecretValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "SecretNotFound" ,
Message : fmt . Sprintf ( ` secret "%s" not found ` , "non-existent-secret" ) ,
ObservedGeneration : 42 ,
} ,
2021-04-15 23:46:27 +00:00
tlsConfigurationValidLoadedTrueCondition ( 42 ) ,
2021-04-12 21:12:51 +00:00
} ,
} ,
} ,
{
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
2021-04-15 23:46:27 +00:00
Phase : "Ready" ,
2021-04-16 00:45:15 +00:00
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
2021-04-12 21:12:51 +00:00
} ,
} ,
} ,
} ,
2021-04-15 21:44:43 +00:00
{
name : "when testing the connection to the LDAP server fails then the upstream is not added to the cache" ,
inputUpstreams : [ ] runtime . Object { validUpstream } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "" ) } ,
2021-04-15 21:44:43 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 ) . Return ( errors . New ( "some bind error" ) )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
2021-04-15 23:46:27 +00:00
bindSecretValidTrueCondition ( 1234 ) ,
2021-04-15 21:44:43 +00:00
{
Type : "LDAPConnectionValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "LDAPConnectionError" ,
Message : fmt . Sprintf (
2021-04-16 00:45:15 +00:00
` could not successfully connect to "%s" and bind as user "%s": error binding as "%s": some bind error ` ,
2021-04-15 21:44:43 +00:00
testHost , testBindUsername , testBindUsername ) ,
ObservedGeneration : 1234 ,
} ,
2021-04-15 23:46:27 +00:00
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
} ,
} ,
} } ,
} ,
{
2021-04-16 00:45:15 +00:00
name : "when the LDAP server connection was already validated for the current resource generation and secret version, then do not validate it again" ,
2021-04-15 23:46:27 +00:00
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Generation = 1234
upstream . Status . Conditions = [ ] v1alpha1 . Condition {
2021-04-16 00:45:15 +00:00
ldapConnectionValidTrueCondition ( 1234 , "4242" ) ,
2021-04-15 23:46:27 +00:00
}
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
2021-04-15 23:46:27 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should not perform a test dial and bind. No mocking here means the test will fail if Bind() or Close() are called.
} ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Ready" ,
2021-04-16 00:45:15 +00:00
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
2021-04-15 23:46:27 +00:00
} ,
} } ,
} ,
{
name : "when the LDAP server connection was validated for an older resource generation, then try to validate it again" ,
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Generation = 1234 // current generation
upstream . Status . Conditions = [ ] v1alpha1 . Condition {
2021-04-16 00:45:15 +00:00
ldapConnectionValidTrueCondition ( 1233 , "4242" ) , // older spec generation!
2021-04-15 23:46:27 +00:00
}
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
2021-04-15 23:46:27 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Ready" ,
2021-04-16 00:45:15 +00:00
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
2021-04-15 23:46:27 +00:00
} ,
} } ,
} ,
{
name : "when the LDAP server connection validation previously failed for this resource generation, then try to validate it again" ,
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Generation = 1234
upstream . Status . Conditions = [ ] v1alpha1 . Condition {
{
Type : "LDAPConnectionValid" ,
Status : "False" , // failure!
LastTransitionTime : now ,
Reason : "LDAPConnectionError" ,
Message : "some-error-message" ,
ObservedGeneration : 1234 , // same (current) generation!
} ,
}
} ) } ,
2021-04-16 00:45:15 +00:00
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Ready" ,
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
} ,
2021-04-15 23:46:27 +00:00
} } ,
2021-04-16 00:45:15 +00:00
} ,
{
name : "when the LDAP server connection was already validated for this resource generation but the bind secret has changed, then try to validate it again" ,
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Generation = 1234
upstream . Status . Conditions = [ ] v1alpha1 . Condition {
ldapConnectionValidTrueCondition ( 1234 , "4241" ) , // same spec generation, old secret version
}
} ) } ,
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } , // newer secret version!
2021-04-15 23:46:27 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a test dial and bind.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Ready" ,
2021-04-16 00:45:15 +00:00
Conditions : allConditionsTrue ( 1234 , "4242" ) ,
2021-04-15 21:44:43 +00:00
} ,
} } ,
} ,
2021-04-16 21:04:05 +00:00
{
name : "when DryRunAuthenticationUsername is specified and a successful dry run authentication is performed" ,
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Spec . DryRunAuthenticationUsername = "endUserUsername"
} ) } ,
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Should perform a full auth dry run.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( gomock . Any ( ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : "testFoundUserDN" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUsernameAttrName , [ ] string { "testDownstreamUsername" } ) ,
ldap . NewEntryAttribute ( testUIDAttrName , [ ] string { "testDownstreamUID" } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { providerConfigForValidUpstream } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Ready" ,
Conditions : [ ] v1alpha1 . Condition {
bindSecretValidTrueCondition ( 1234 ) ,
{
Type : "LDAPConnectionValid" ,
Status : "True" ,
LastTransitionTime : now ,
Reason : "Success" ,
Message : fmt . Sprintf (
` successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"] ` ,
"endUserUsername" , "testDownstreamUsername" , "testDownstreamUID" , testSecretName , "4242" ) ,
ObservedGeneration : 1234 ,
} ,
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
} ,
} ,
} } ,
} ,
{
name : "when DryRunAuthenticationUsername is specified and the dry run authentication returns an error" ,
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Spec . DryRunAuthenticationUsername = "endUserUsername"
} ) } ,
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Failure during a full auth dry run.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( gomock . Any ( ) ) . Return ( nil , errors . New ( "some dry run error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
bindSecretValidTrueCondition ( 1234 ) ,
{
Type : "LDAPConnectionValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "AuthenticationDryRunError" ,
Message : fmt . Sprintf (
` failed authentication dry run for end user "%s": error searching for user "%s": some dry run error ` ,
"endUserUsername" , "endUserUsername" ) ,
ObservedGeneration : 1234 ,
} ,
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
} ,
} ,
} } ,
} ,
{
name : "when DryRunAuthenticationUsername is specified and the dry run authentication returns unauthenticated without an error" ,
inputUpstreams : [ ] runtime . Object { editedValidUpstream ( func ( upstream * v1alpha1 . LDAPIdentityProvider ) {
upstream . Spec . DryRunAuthenticationUsername = "endUserUsername"
} ) } ,
inputSecrets : [ ] runtime . Object { validBindUserSecret ( "4242" ) } ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
// Failure during full auth dry run which will cause it to return unauthenticated instead of error.
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( gomock . Any ( ) ) . Return ( & ldap . SearchResult {
// No search results means the user did not enter a valid username, which is unauthenticated instead of error.
Entries : [ ] * ldap . Entry { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : controllerlib . ErrSyntheticRequeue . Error ( ) ,
wantResultingCache : [ ] * upstreamldap . ProviderConfig { } ,
wantResultingUpstreams : [ ] v1alpha1 . LDAPIdentityProvider { {
ObjectMeta : metav1 . ObjectMeta { Namespace : testNamespace , Name : testName , Generation : 1234 } ,
Status : v1alpha1 . LDAPIdentityProviderStatus {
Phase : "Error" ,
Conditions : [ ] v1alpha1 . Condition {
bindSecretValidTrueCondition ( 1234 ) ,
{
Type : "LDAPConnectionValid" ,
Status : "False" ,
LastTransitionTime : now ,
Reason : "AuthenticationDryRunError" ,
Message : fmt . Sprintf (
` failed authentication dry run for end user "%s": user not found ` ,
"endUserUsername" ) ,
ObservedGeneration : 1234 ,
} ,
tlsConfigurationValidLoadedTrueCondition ( 1234 ) ,
} ,
} ,
} } ,
} ,
2021-04-10 01:49:43 +00:00
}
2021-04-16 21:04:05 +00:00
2021-04-10 01:49:43 +00:00
for _ , tt := range tests {
tt := tt
t . Run ( tt . name , func ( t * testing . T ) {
t . Parallel ( )
2021-04-15 21:44:43 +00:00
2021-04-10 01:49:43 +00:00
fakePinnipedClient := pinnipedfake . NewSimpleClientset ( tt . inputUpstreams ... )
pinnipedInformers := pinnipedinformers . NewSharedInformerFactory ( fakePinnipedClient , 0 )
fakeKubeClient := fake . NewSimpleClientset ( tt . inputSecrets ... )
kubeInformers := informers . NewSharedInformerFactory ( fakeKubeClient , 0 )
cache := provider . NewDynamicUpstreamIDPProvider ( )
cache . SetLDAPIdentityProviders ( [ ] provider . UpstreamLDAPIdentityProviderI {
2021-04-15 17:25:35 +00:00
upstreamldap . New ( upstreamldap . ProviderConfig { Name : "initial-entry" } ) ,
2021-04-10 01:49:43 +00:00
} )
2021-04-15 21:44:43 +00:00
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
conn := mockldapconn . NewMockConn ( ctrl )
if tt . setupMocks != nil {
tt . setupMocks ( conn )
}
dialer := & comparableDialer { f : upstreamldap . LDAPDialerFunc ( func ( ctx context . Context , _ string ) ( upstreamldap . Conn , error ) {
if tt . dialError != nil {
return nil , tt . dialError
}
return conn , nil
} ) }
2021-04-27 19:43:09 +00:00
controller := newInternal (
2021-04-10 01:49:43 +00:00
cache ,
2021-04-15 21:44:43 +00:00
dialer ,
2021-04-10 01:49:43 +00:00
fakePinnipedClient ,
pinnipedInformers . IDP ( ) . V1alpha1 ( ) . LDAPIdentityProviders ( ) ,
kubeInformers . Core ( ) . V1 ( ) . Secrets ( ) ,
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 )
}
actualIDPList := cache . GetLDAPIdentityProviders ( )
require . Equal ( t , len ( tt . wantResultingCache ) , len ( actualIDPList ) )
for i := range actualIDPList {
actualIDP := actualIDPList [ i ] . ( * upstreamldap . Provider )
2021-04-15 21:44:43 +00:00
copyOfExpectedValue := * tt . wantResultingCache [ i ] // copy before edit to avoid race because these tests are run in parallel
// The dialer that was passed in to the controller's constructor should always have been
// passed through to the provider.
copyOfExpectedValue . Dialer = dialer
require . Equal ( t , copyOfExpectedValue , actualIDP . GetConfig ( ) )
2021-04-10 01:49:43 +00:00
}
actualUpstreams , err := fakePinnipedClient . IDPV1alpha1 ( ) . LDAPIdentityProviders ( testNamespace ) . List ( ctx , metav1 . ListOptions { } )
require . NoError ( t , err )
2021-04-12 20:53:21 +00:00
// Assert on the expected Status of the upstreams. Preprocess the upstreams a bit so that they're easier to assert against.
normalizedActualUpstreams := normalizeLDAPUpstreams ( actualUpstreams . Items , now )
require . Equal ( t , len ( tt . wantResultingUpstreams ) , len ( normalizedActualUpstreams ) )
for i := range tt . wantResultingUpstreams {
// Require each separately to get a nice diff when the test fails.
require . Equal ( t , tt . wantResultingUpstreams [ i ] , normalizedActualUpstreams [ i ] )
}
2021-04-10 01:49:43 +00:00
} )
}
}
2021-04-12 20:53:21 +00:00
func normalizeLDAPUpstreams ( upstreams [ ] v1alpha1 . LDAPIdentityProvider , now metav1 . Time ) [ ] v1alpha1 . LDAPIdentityProvider {
result := make ( [ ] v1alpha1 . LDAPIdentityProvider , 0 , len ( upstreams ) )
for _ , u := range upstreams {
normalized := u . DeepCopy ( )
// We're only interested in comparing the status, so zero out the spec.
normalized . Spec = v1alpha1 . LDAPIdentityProviderSpec { }
// 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 )
}
2021-04-12 21:12:51 +00:00
sort . SliceStable ( result , func ( i , j int ) bool {
return result [ i ] . Name < result [ j ] . Name
} )
2021-04-12 20:53:21 +00:00
return result
}