Update federation_domain_watcher with new IdentityProviderFound

- adds the truthy condition
- TODOs for falsy conditions
- addiional notes for other conditions
- tests updated to pass with the new condition

Co-authored-by: Ryan Richard <richardry@vmware.com>
This commit is contained in:
Benjamin A. Petersen 2023-07-07 17:10:07 -04:00 committed by Ryan Richard
parent 48e44e13c6
commit e9fb4242d5
2 changed files with 197 additions and 64 deletions

View File

@ -35,6 +35,7 @@ const (
typeIssuerURLValid = "IssuerURLValid" typeIssuerURLValid = "IssuerURLValid"
typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname" typeOneTLSSecretPerIssuerHostname = "OneTLSSecretPerIssuerHostname"
typeIssuerIsUnique = "IssuerIsUnique" typeIssuerIsUnique = "IssuerIsUnique"
typeIdentityProvidersFound = "IdentityProvidersFound"
reasonSuccess = "Success" reasonSuccess = "Success"
reasonNotReady = "NotReady" reasonNotReady = "NotReady"
@ -42,6 +43,7 @@ const (
reasonInvalidIssuerURL = "InvalidIssuerURL" reasonInvalidIssuerURL = "InvalidIssuerURL"
reasonDuplicateIssuer = "DuplicateIssuer" reasonDuplicateIssuer = "DuplicateIssuer"
reasonDifferentSecretRefsFound = "DifferentSecretRefsFound" reasonDifferentSecretRefsFound = "DifferentSecretRefsFound"
reasonLegacyConfigurationSuccess = "LegacyConfigurationSuccess"
celTransformerMaxExpressionRuntime = 5 * time.Second celTransformerMaxExpressionRuntime = 5 * time.Second
) )
@ -154,30 +156,57 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro
// Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type. // Check if that there is exactly one IDP defined in the Supervisor namespace of any IDP CRD type.
idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders) idpCRsCount := len(oidcIdentityProviders) + len(ldapIdentityProviders) + len(activeDirectoryIdentityProviders)
if idpCRsCount == 1 { if idpCRsCount == 1 {
foundIDPName := ""
// If so, default that IDP's DisplayName to be the same as its resource Name. // If so, default that IDP's DisplayName to be the same as its resource Name.
defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{} defaultFederationDomainIdentityProvider = &federationdomainproviders.FederationDomainIdentityProvider{}
switch { switch {
case len(oidcIdentityProviders) == 1: case len(oidcIdentityProviders) == 1:
defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name defaultFederationDomainIdentityProvider.DisplayName = oidcIdentityProviders[0].Name
defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID defaultFederationDomainIdentityProvider.UID = oidcIdentityProviders[0].UID
foundIDPName = oidcIdentityProviders[0].Name
case len(ldapIdentityProviders) == 1: case len(ldapIdentityProviders) == 1:
defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name defaultFederationDomainIdentityProvider.DisplayName = ldapIdentityProviders[0].Name
defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID defaultFederationDomainIdentityProvider.UID = ldapIdentityProviders[0].UID
foundIDPName = ldapIdentityProviders[0].Name
case len(activeDirectoryIdentityProviders) == 1: case len(activeDirectoryIdentityProviders) == 1:
defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name defaultFederationDomainIdentityProvider.DisplayName = activeDirectoryIdentityProviders[0].Name
defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID defaultFederationDomainIdentityProvider.UID = activeDirectoryIdentityProviders[0].UID
foundIDPName = activeDirectoryIdentityProviders[0].Name
} }
// Backwards compatibility mode always uses an empty identity transformation pipline since no // Backwards compatibility mode always uses an empty identity transformation pipline since no
// transformations are defined on the FederationDomain. // transformations are defined on the FederationDomain.
defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline() defaultFederationDomainIdentityProvider.Transforms = idtransform.NewTransformationPipeline()
conditions = append(conditions, &configv1alpha1.Condition{
Type: typeIdentityProvidersFound,
Status: configv1alpha1.ConditionTrue,
Reason: reasonLegacyConfigurationSuccess,
Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+
"identity provider resource has been found: using %q as "+
"identity provider: please explicitly list identity providers in .spec.identityProviders "+
"(this legacy configuration mode may be removed in a future version of Pinniped)", foundIDPName),
})
plog.Warning("detected FederationDomain identity provider backwards compatibility mode: using the one existing identity provider for authentication", plog.Warning("detected FederationDomain identity provider backwards compatibility mode: using the one existing identity provider for authentication",
"federationDomain", federationDomain.Name) "federationDomain", federationDomain.Name)
} else { } else {
// TODO(BEN): add the "falsy" status versions of this condition and add tests to cover.
// this matches the above "truthy" version of the condition above.
//
// There are no IDP CRs or there is more than one IDP CR. Either way, we are not in the backwards // There are no IDP CRs or there is more than one IDP CR. Either way, we are not in the backwards
// compatibility mode because there is not exactly one IDP CR in the namespace, despite the fact that no // compatibility mode because there is not exactly one IDP CR in the namespace, despite the fact that no
// IDPs are listed on the FederationDomain. Create a FederationDomain which has no IDPs and therefore // IDPs are listed on the FederationDomain. Create a FederationDomain which has no IDPs and therefore
// cannot actually be used to log in, but still serves endpoints. // cannot actually be used to log in, but still serves endpoints.
// TODO: Write something into the FederationDomain's status to explain what's happening and how to fix it. // TODO: Write something into the FederationDomain's status to explain what's happening and how to fix it.
// write code for these two condition~
// Type: IdentityProvidersFound
// Reason: LegacyConfigurationIdentityProviderNotFound
// Message: "no resources were specified by .spec.identityProviders[].objectRef and {number.of.idp.resources} identity provider resources have been found: please update .spec.identityProviders to specify which identity providers this federation domain should use"
// Status: false
//
// Type: IdentityProvidersFound
// Reason: LegacyConfigurationIdentityProviderNotFound
// Message: "no resources were specified by .spec.identityProviders[].objectRef and no identity provider resources have been found: please create an identity provider resource"
// status: false
//
plog.Warning("FederationDomain has no identity providers listed and there is not exactly one identity provider defined in the namespace: authentication disabled", plog.Warning("FederationDomain has no identity providers listed and there is not exactly one identity provider defined in the namespace: authentication disabled",
"federationDomain", federationDomain.Name, "federationDomain", federationDomain.Name,
"namespace", federationDomain.Namespace, "namespace", federationDomain.Namespace,
@ -354,6 +383,18 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro
"identityProviderResourceUID", idpResourceUID, "identityProviderResourceUID", idpResourceUID,
) )
} }
// TODO:
// - for the new "non-legacy" version of this to pass, we need to do this work. however, we don't need this for all of the tests for legacy functionality to work.
// Type: IdentityProvidersFound
// Reason: IdentityProvidersObjectRefsNotFound
// Message: ".spec.identityProviders[].objectRef identifies resource(s) that cannot be found: {list.of.specific.resources.that.cant.be.found.in.case.some.can.be.found.but.not.all}"
// Status: false
//
// TODO: maaaaybe we need this happy case as well for the legacy functionality tests to fully pass?
// Type: IdentityProvidersFound
// Reason: Success
// Message: Non-legacy happy state "the resources specified by .spec.identityProviders[].objectRef were found"
// Status: true
// Now that we have the list of IDPs for this FederationDomain, create the issuer. // Now that we have the list of IDPs for this FederationDomain, create the issuer.
var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer var federationDomainIssuer *federationdomainproviders.FederationDomainIssuer
@ -392,7 +433,10 @@ func (c *federationDomainWatcherController) Sync(ctx controllerlib.Context) erro
federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer) federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer)
} }
} }
// BEN: notes:
// implementer of this function is the endpoint manager.
// it should re-setup all of the endpoints for the specified federation domains.
// (in test, this is wantFederationDomainIssues)
c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...) c.federationDomainsSetter.SetFederationDomains(federationDomainIssuers...)
return errors.NewAggregate(errs) return errors.NewAggregate(errs)

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/url" "net/url"
"sort"
"testing" "testing"
"time" "time"
@ -25,6 +26,7 @@ import (
pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions" pinnipedinformers "go.pinniped.dev/generated/latest/client/supervisor/informers/externalversions"
"go.pinniped.dev/internal/controllerlib" "go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/federationdomain/federationdomainproviders" "go.pinniped.dev/internal/federationdomain/federationdomainproviders"
"go.pinniped.dev/internal/idtransform"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
) )
@ -129,33 +131,39 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
Resource: "federationdomains", Resource: "federationdomains",
} }
identityProvider := &idpv1alpha1.OIDCIdentityProvider{
ObjectMeta: metav1.ObjectMeta{
Name: "some-name",
UID: "some-uid",
},
}
federationDomain1 := &configv1alpha1.FederationDomain{ federationDomain1 := &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123}, ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer1.com"},
} }
federationDomain1Issuer, err := federationdomainproviders.NewFederationDomainIssuer(
federationDomain1.Spec.Issuer,
[]*federationdomainproviders.FederationDomainIdentityProvider{},
)
require.NoError(t, err)
federationDomain2 := &configv1alpha1.FederationDomain{ federationDomain2 := &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123}, ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer2.com"},
} }
federationDomain2Issuer, err := federationdomainproviders.NewFederationDomainIssuer(
federationDomain2.Spec.Issuer,
[]*federationdomainproviders.FederationDomainIdentityProvider{},
)
require.NoError(t, err)
invalidFederationDomain := &configv1alpha1.FederationDomain{ invalidFederationDomain := &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123}, ObjectMeta: metav1.ObjectMeta{Name: "invalid-config", Namespace: namespace, Generation: 123},
Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://invalid-issuer.com?some=query"},
} }
federationDomainIssuerWithDefaultIDP := func(t *testing.T, fedDomainIssuer string, idpObjectMeta metav1.ObjectMeta) *federationdomainproviders.FederationDomainIssuer {
fdIDP := &federationdomainproviders.FederationDomainIdentityProvider{
DisplayName: idpObjectMeta.Name,
UID: idpObjectMeta.UID,
Transforms: idtransform.NewTransformationPipeline(),
}
fdIssuer, err := federationdomainproviders.NewFederationDomainIssuerWithDefaultIDP(fedDomainIssuer, fdIDP)
require.NoError(t, err)
return fdIssuer
}
happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition { happyReadyCondition := func(issuer string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition {
return configv1alpha1.Condition{ return configv1alpha1.Condition{
Type: "Ready", Type: "Ready",
@ -278,8 +286,26 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
} }
} }
allHappyConditions := func(issuer string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition { happyIdentityProvidersFoundConditionLegacyConfigurationSuccess := func(idpName string, time metav1.Time, observedGeneration int64) configv1alpha1.Condition {
return configv1alpha1.Condition{
Type: "IdentityProvidersFound",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "LegacyConfigurationSuccess",
Message: fmt.Sprintf("no resources were specified by .spec.identityProviders[].objectRef but exactly one "+
"identity provider resource has been found: using %q as "+
"identity provider: please explicitly list identity providers in .spec.identityProviders "+
"(this legacy configuration mode may be removed in a future version of Pinniped)", idpName),
}
}
// sadIdentityProvidersFoundConditionForSomeReasons := func() {}
allHappyConditionsLegacyConfigurationSuccess := func(issuer, idpName string, time metav1.Time, observedGeneration int64) []configv1alpha1.Condition {
return []configv1alpha1.Condition{ return []configv1alpha1.Condition{
// sorted alphabetically by type
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration),
happyIssuerIsUniqueCondition(time, observedGeneration), happyIssuerIsUniqueCondition(time, observedGeneration),
happyIssuerURLValidCondition(time, observedGeneration), happyIssuerURLValidCondition(time, observedGeneration),
happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration), happyOneTLSSecretPerIssuerHostnameCondition(time, observedGeneration),
@ -288,7 +314,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
} }
invalidIssuerURL := ":/host//path" invalidIssuerURL := ":/host//path"
_, err = url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid. _, err := url.Parse(invalidIssuerURL) //nolint:staticcheck // Yes, this URL is intentionally invalid.
require.Error(t, err) require.Error(t, err)
newCopyWithStatus := func( newCopyWithStatus := func(
@ -311,7 +337,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
wantFederationDomainIssuers func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer wantFederationDomainIssuers func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer
}{ }{
{ {
name: "there are no FederationDomains", name: "when there are no FederationDomains, nothing happens",
inputObjects: []runtime.Object{}, inputObjects: []runtime.Object{},
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
return []*federationdomainproviders.FederationDomainIssuer{} return []*federationdomainproviders.FederationDomainIssuer{}
@ -321,15 +347,25 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
}, },
{ {
name: "there are some valid FederationDomains in the informer", // TODO: fill in these conditions in my TODO blocks.
// conditions = append(conditions, &configv1alpha1.Condition{
// Type: typeIssuerURLValid,
// Status: configv1alpha1.ConditionFalse,
// Reason: reasonInvalidIssuerURL,
// Message: err.Error(),
// })
name: "legacy config: when no identity provider is specified on federation domains, but exactly one identity " +
"provider resource exists on cluster, the controller will set a default IDP on each federation domain " +
"matching the only identity provider found",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
federationDomain1, federationDomain1,
federationDomain2, federationDomain2,
identityProvider,
}, },
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
federationDomain1Issuer, federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider.ObjectMeta),
federationDomain2Issuer, federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -337,30 +373,32 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace,
newCopyWithStatus(federationDomain1, newCopyWithStatus(federationDomain1,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace,
newCopyWithStatus(federationDomain2, newCopyWithStatus(federationDomain2,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
{ {
name: "there are two valid FederationDomains, but one is already up to date, so only updates the out-of-date FederationDomain", name: "when there are two valid FederationDomains, but one is already up to date, the sync loop only updates " +
"the out-of-date FederationDomain",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
identityProvider,
newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady, newCopyWithStatus(federationDomain1, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
federationDomain2, federationDomain2,
}, },
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
federationDomain1Issuer, federationDomainIssuerWithDefaultIDP(t, federationDomain1.Spec.Issuer, identityProvider.ObjectMeta),
federationDomain2Issuer, federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -368,17 +406,18 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace,
newCopyWithStatus(federationDomain2, newCopyWithStatus(federationDomain2,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
{ {
name: "there are two valid FederationDomains, but updating one fails", name: "when there are two valid FederationDomains, but updating one fails, the status on the FederationDomain will not change",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
federationDomain1, federationDomain1,
federationDomain2, federationDomain2,
identityProvider,
}, },
configPinnipedClient: func(client *pinnipedfake.Clientset) { configPinnipedClient: func(client *pinnipedfake.Clientset) {
client.PrependReactor( client.PrependReactor(
@ -396,7 +435,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
wantErr: "could not update status: some update error", wantErr: "could not update status: some update error",
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
federationDomain2Issuer, // federationDomain1 is not included because it encountered an error // federationDomain1 is not included because it encountered an error
federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -404,27 +444,30 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain1.Namespace,
newCopyWithStatus(federationDomain1, newCopyWithStatus(federationDomain1,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain1.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace,
newCopyWithStatus(federationDomain2, newCopyWithStatus(federationDomain2,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
{ {
name: "there are both valid and invalid FederationDomains", name: "when there are both valid and invalid FederationDomains, the status will be correctly set on each " +
"FederationDomain individually",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
invalidFederationDomain, invalidFederationDomain,
federationDomain2, federationDomain2,
identityProvider,
}, },
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
federationDomain2Issuer, // only the valid FederationDomain // only the valid FederationDomain
federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -433,6 +476,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
newCopyWithStatus(invalidFederationDomain, newCopyWithStatus(invalidFederationDomain,
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123),
sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123),
happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -443,17 +487,19 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace,
newCopyWithStatus(federationDomain2, newCopyWithStatus(federationDomain2,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
{ {
name: "there are both valid and invalid FederationDomains, but updating the invalid one fails", name: "when there are both valid and invalid FederationDomains, but updating the invalid one fails, the " +
"existing status will be unchanged",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
invalidFederationDomain, invalidFederationDomain,
federationDomain2, federationDomain2,
identityProvider,
}, },
configPinnipedClient: func(client *pinnipedfake.Clientset) { configPinnipedClient: func(client *pinnipedfake.Clientset) {
client.PrependReactor( client.PrependReactor(
@ -471,7 +517,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
wantErr: "could not update status: some update error", wantErr: "could not update status: some update error",
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
federationDomain2Issuer, // only the valid FederationDomain // only the valid FederationDomain
federationDomainIssuerWithDefaultIDP(t, federationDomain2.Spec.Issuer, identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -480,6 +527,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
newCopyWithStatus(invalidFederationDomain, newCopyWithStatus(invalidFederationDomain,
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123),
sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123),
happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -490,14 +538,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace, coretesting.NewUpdateSubresourceAction(federationDomainGVR, "status", federationDomain2.Namespace,
newCopyWithStatus(federationDomain2, newCopyWithStatus(federationDomain2,
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions(federationDomain2.Spec.Issuer, frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
{ {
name: "there are FederationDomains with duplicate issuer names", name: "when there are FederationDomains with duplicate issuer strings these particular FederationDomains " +
"will report error on IssuerUnique conditions",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
&configv1alpha1.FederationDomain{ &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123}, ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123},
@ -511,15 +560,12 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123}, ObjectMeta: metav1.ObjectMeta{Name: "not-duplicate", Namespace: namespace, Generation: 123},
Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive) Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, // different path (paths are case-sensitive)
}, },
identityProvider,
}, },
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
fdi, err := federationdomainproviders.NewFederationDomainIssuer(
"https://issuer-duplicate.com/A",
[]*federationdomainproviders.FederationDomainIdentityProvider{},
)
require.NoError(t, err)
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
fdi, // different path (paths are case-sensitive)
federationDomainIssuerWithDefaultIDP(t, "https://issuer-duplicate.com/A", identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -532,6 +578,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
happyIssuerURLValidCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123),
happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -547,6 +594,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
sadIssuerIsUniqueCondition(frozenMetav1Now, 123), sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
happyIssuerURLValidCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123),
happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), happyOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -561,14 +609,15 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"}, Spec: configv1alpha1.FederationDomainSpec{Issuer: "https://issuer-duplicate.com/A"},
}, },
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions("https://issuer-duplicate.com/A", frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/A", identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
{ {
name: "there are FederationDomains with the same issuer DNS hostname using different secretNames", name: "when there are FederationDomains with the same issuer DNS hostname using different secretNames these " +
"particular FederationDomains will report errors on OneTLSSecretPerIssuerHostname conditions",
inputObjects: []runtime.Object{ inputObjects: []runtime.Object{
&configv1alpha1.FederationDomain{ &configv1alpha1.FederationDomain{
ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123}, ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123},
@ -600,15 +649,11 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"}, TLS: &configv1alpha1.FederationDomainTLSSpec{SecretName: "secret1"},
}, },
}, },
identityProvider,
}, },
wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer { wantFederationDomainIssuers: func(t *testing.T) []*federationdomainproviders.FederationDomainIssuer {
fdi, err := federationdomainproviders.NewFederationDomainIssuer(
"https://issuer-not-duplicate.com",
[]*federationdomainproviders.FederationDomainIdentityProvider{},
)
require.NoError(t, err)
return []*federationdomainproviders.FederationDomainIssuer{ return []*federationdomainproviders.FederationDomainIssuer{
fdi, federationDomainIssuerWithDefaultIDP(t, "https://issuer-not-duplicate.com", identityProvider.ObjectMeta),
} }
}, },
wantActions: func(t *testing.T) []coretesting.Action { wantActions: func(t *testing.T) []coretesting.Action {
@ -624,6 +669,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123),
happyIssuerURLValidCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123),
sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -642,6 +688,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
happyIssuerIsUniqueCondition(frozenMetav1Now, 123), happyIssuerIsUniqueCondition(frozenMetav1Now, 123),
happyIssuerURLValidCondition(frozenMetav1Now, 123), happyIssuerURLValidCondition(frozenMetav1Now, 123),
sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -660,6 +707,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
configv1alpha1.FederationDomainPhaseError, configv1alpha1.FederationDomainPhaseError,
[]configv1alpha1.Condition{ []configv1alpha1.Condition{
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(identityProvider.Name, frozenMetav1Now, 123),
unknownIssuerIsUniqueCondition(frozenMetav1Now, 123), unknownIssuerIsUniqueCondition(frozenMetav1Now, 123),
sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123), sadIssuerURLValidConditionCannotParse(frozenMetav1Now, 123),
unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123), unknownOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
@ -677,12 +725,30 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
}, },
}, },
configv1alpha1.FederationDomainPhaseReady, configv1alpha1.FederationDomainPhaseReady,
allHappyConditions("https://issuer-not-duplicate.com", frozenMetav1Now, 123), allHappyConditionsLegacyConfigurationSuccess("https://issuer-not-duplicate.com", identityProvider.Name, frozenMetav1Now, 123),
), ),
), ),
} }
}, },
}, },
// TODO(Ben): add these additional tests to cover the new cases. There will likely also be more as we cover
// both the truthy as well as the falsy cases.
// {
// name: "legacy config: no identity provider specified in federation domain and no identity providers found",
// wantErr: "...please create an identity provider resource",
// },
// {
// name: "legacy config: no identity provider specified in federation domain and multiple identity providers found",
// wantErr: "...to specify which identity providers this federation domain should use",
// },
// {
// name: "the federation domain specifies identity providers that cannot be found", // single and/or multiple?
// wantErr: "...identifies resource(s) that cannot be found: {list.of...}",
// },
// {
// name: "the federation domain specifies identity providers taht exist",
// wantErr: "", // n/a
// },
} }
for _, tt := range tests { for _, tt := range tests {
@ -727,18 +793,41 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
} }
if tt.wantActions != nil {
require.ElementsMatch(t, tt.wantActions(t), pinnipedAPIClient.Actions())
} else {
require.Empty(t, pinnipedAPIClient.Actions())
}
if tt.wantFederationDomainIssuers != nil { if tt.wantFederationDomainIssuers != nil {
require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled) require.True(t, federationDomainsSetter.SetFederationDomainsWasCalled)
require.ElementsMatch(t, tt.wantFederationDomainIssuers(t), federationDomainsSetter.FederationDomainsReceived) require.ElementsMatch(t, tt.wantFederationDomainIssuers(t), federationDomainsSetter.FederationDomainsReceived)
} else { } else {
require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled) require.False(t, federationDomainsSetter.SetFederationDomainsWasCalled)
} }
if tt.wantActions != nil {
// In this controller we don't actually care about the order of the actions, the FederationDomains
// can be updated in any order. Therefore, we are sorting here to make the test output easier to read.
// Unfortunately the timezone nested in the condition still makes it pretty ugly.
actualActions := pinnipedAPIClient.Actions()
sortActions(t, actualActions)
wantedActions := tt.wantActions(t)
sortActions(t, wantedActions)
require.Equal(t, wantedActions, actualActions)
} else {
require.Empty(t, pinnipedAPIClient.Actions())
}
}) })
} }
} }
func sortActions(t *testing.T, actions []coretesting.Action) {
sort.SliceStable(actions, func(prev, next int) bool {
updateAction1, ok := actions[prev].(coretesting.UpdateAction)
require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction for sort comparison %#v", actions[prev])
obj1, ok := updateAction1.GetObject().(metav1.Object)
require.True(t, ok, "failed to cast an action as a metav1.Object for sort comparison %#v", actions[prev])
updateAction2, ok := actions[next].(coretesting.UpdateAction)
require.True(t, ok, "failed to cast an action as an coretesting.UpdateAction for sort comparison %#v", actions[next])
obj2, ok := updateAction2.GetObject().(metav1.Object)
require.True(t, ok, "failed to cast an action as a metav1.Object for sort comparison %#v", actions[next])
return obj1.GetName() < obj2.GetName()
})
}