internal/groupsuffix: mutate TokenCredentialRequest's Authenticator

This is a partial revert of 288d9c999e. For some reason it didn't occur to me
that we could do it this way earlier. Whoops.

This also contains a middleware update: mutation funcs can return an error now
and short-circuit the rest of the request/response flow. The idea here is that
if someone is configuring their kubeclient to use middleware, they are agreeing
to a narrow-er client contract by doing so (e.g., their TokenCredentialRequest's
must have an Spec.Authenticator.APIGroup set).

I also updated some internal/groupsuffix tests to be more realistic.

Signed-off-by: Andrew Keesler <akeesler@vmware.com>
This commit is contained in:
Andrew Keesler 2021-02-04 20:02:59 -05:00
parent ae6503e972
commit 0fc1f17866
No known key found for this signature in database
GPG Key ID: 27CE0444346F9413
14 changed files with 453 additions and 195 deletions

View File

@ -5,6 +5,7 @@ package groupsuffix
import ( import (
"context" "context"
"fmt"
"strings" "strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -12,8 +13,10 @@ import (
"k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/constable" "go.pinniped.dev/internal/constable"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
"go.pinniped.dev/internal/plog"
) )
const ( const (
@ -35,7 +38,7 @@ func New(apiGroupSuffix string) kubeclient.Middleware {
return // ignore APIs that do not have our group return // ignore APIs that do not have our group
} }
rt.MutateRequest(func(obj kubeclient.Object) { rt.MutateRequest(func(obj kubeclient.Object) error {
typeMeta := obj.GetObjectKind() typeMeta := obj.GetObjectKind()
origGVK := typeMeta.GroupVersionKind() origGVK := typeMeta.GroupVersionKind()
newGVK := schema.GroupVersionKind{ newGVK := schema.GroupVersionKind{
@ -44,6 +47,7 @@ func New(apiGroupSuffix string) kubeclient.Middleware {
Kind: origGVK.Kind, Kind: origGVK.Kind,
} }
typeMeta.SetGroupVersionKind(newGVK) typeMeta.SetGroupVersionKind(newGVK)
return nil
}) })
}), }),
@ -61,6 +65,49 @@ func New(apiGroupSuffix string) kubeclient.Middleware {
rt.MutateRequest(mutateOwnerRefs(Replace, apiGroupSuffix)) rt.MutateRequest(mutateOwnerRefs(Replace, apiGroupSuffix))
}), }),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// we only care if this is a create on a TokenCredentialRequest without a subresource
if rt.Resource() != loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests") ||
rt.Verb() != kubeclient.VerbCreate ||
rt.Subresource() != "" {
return
}
// we only do this on the way out, since on the way back in we don't set a spec in our
// TokenCredentialRequest
rt.MutateRequest(func(obj kubeclient.Object) error {
tokenCredentialRequest, ok := obj.(*loginv1alpha1.TokenCredentialRequest)
if !ok {
return fmt.Errorf("cannot cast obj of type %T to *loginv1alpha1.TokenCredentialRequest", obj)
}
if tokenCredentialRequest.Spec.Authenticator.APIGroup == nil {
// technically, the APIGroup field is optional, so clients are free to do this, but we
// want our middleware to be opinionated so that it can be really good at a specific task
// and give us specific feedback when it can't do that specific task
return fmt.Errorf(
"cannot replace token credential request %q without authenticator API group",
plog.KObj(obj),
)
}
mutatedAuthenticatorAPIGroup, ok := Replace(*tokenCredentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix)
if !ok {
// see comment above about specificity of middleware
return fmt.Errorf(
"cannot replace token credential request %q authenticator API group %q with group suffix %q",
plog.KObj(obj),
*tokenCredentialRequest.Spec.Authenticator.APIGroup,
apiGroupSuffix,
)
}
tokenCredentialRequest.Spec.Authenticator.APIGroup = &mutatedAuthenticatorAPIGroup
return nil
})
}),
kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) { kubeclient.MiddlewareFunc(func(_ context.Context, rt kubeclient.RoundTrip) {
// always unreplace owner refs with apiGroupSuffix because we can consume those objects across all verbs // always unreplace owner refs with apiGroupSuffix because we can consume those objects across all verbs
rt.MutateResponse(mutateOwnerRefs(Unreplace, apiGroupSuffix)) rt.MutateResponse(mutateOwnerRefs(Unreplace, apiGroupSuffix))
@ -68,12 +115,12 @@ func New(apiGroupSuffix string) kubeclient.Middleware {
} }
} }
func mutateOwnerRefs(replaceFunc func(baseAPIGroup, apiGroupSuffix string) (string, bool), apiGroupSuffix string) func(kubeclient.Object) { func mutateOwnerRefs(replaceFunc func(baseAPIGroup, apiGroupSuffix string) (string, bool), apiGroupSuffix string) func(kubeclient.Object) error {
return func(obj kubeclient.Object) { return func(obj kubeclient.Object) error {
// fix up owner refs because they are consumed by external and internal actors // fix up owner refs because they are consumed by external and internal actors
oldRefs := obj.GetOwnerReferences() oldRefs := obj.GetOwnerReferences()
if len(oldRefs) == 0 { if len(oldRefs) == 0 {
return return nil
} }
var changedGroup bool var changedGroup bool
@ -94,10 +141,12 @@ func mutateOwnerRefs(replaceFunc func(baseAPIGroup, apiGroupSuffix string) (stri
} }
if !changedGroup { if !changedGroup {
return return nil
} }
obj.SetOwnerReferences(newRefs) obj.SetOwnerReferences(newRefs)
return nil
} }
} }

View File

@ -14,6 +14,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
authv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/authentication/v1alpha1"
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1" configv1alpha1 "go.pinniped.dev/generated/1.20/apis/supervisor/config/v1alpha1"
"go.pinniped.dev/internal/kubeclient" "go.pinniped.dev/internal/kubeclient"
@ -31,6 +32,7 @@ func ExampleReplace_string() {
fmt.Println(s) fmt.Println(s)
// Output: idp.supervisor.marlin.io // Output: idp.supervisor.marlin.io
} }
func TestMiddlware(t *testing.T) { func TestMiddlware(t *testing.T) {
const newSuffix = "some.suffix.com" const newSuffix = "some.suffix.com"
@ -117,14 +119,14 @@ func TestMiddlware(t *testing.T) {
}, },
}, },
} }
federationDomainWithPinnipedOwnerWithNewGroup := &configv1alpha1.FederationDomain{ federationDomainWithNewGroupAndPinnipedOwner := &configv1alpha1.FederationDomain{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
APIVersion: configv1alpha1.SchemeGroupVersion.String(), APIVersion: replaceGV(t, configv1alpha1.SchemeGroupVersion, newSuffix).String(),
Kind: "FederationDomain", Kind: "FederationDomain",
}, },
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
OwnerReferences: []metav1.OwnerReference{ OwnerReferences: []metav1.OwnerReference{
*metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerWithNewGroupGVK), *metav1.NewControllerRef(pinnipedOwner, pinnipedOwnerGVK),
// make sure we don't update the non-pinniped owner // make sure we don't update the non-pinniped owner
*metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK), *metav1.NewControllerRef(nonPinnipedOwner, nonPinnipedOwnerGVK),
@ -133,7 +135,7 @@ func TestMiddlware(t *testing.T) {
} }
federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup := &configv1alpha1.FederationDomain{ federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup := &configv1alpha1.FederationDomain{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
APIVersion: replaceGV(t, configv1alpha1.SchemeGroupVersion, newSuffix), APIVersion: replaceGV(t, configv1alpha1.SchemeGroupVersion, newSuffix).String(),
Kind: "FederationDomain", Kind: "FederationDomain",
}, },
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@ -146,29 +148,40 @@ func TestMiddlware(t *testing.T) {
}, },
} }
tokenCredentialRequest := &loginv1alpha1.TokenCredentialRequest{ tokenCredentialRequest := with(
TypeMeta: metav1.TypeMeta{ &loginv1alpha1.TokenCredentialRequest{},
APIVersion: loginv1alpha1.SchemeGroupVersion.String(), gvk(replaceGV(t, loginv1alpha1.SchemeGroupVersion, newSuffix).WithKind("TokenCredentialRequest")),
Kind: "TokenCredentialRequest", )
}, tokenCredentialRequestWithPinnipedAuthenticator := with(
} tokenCredentialRequest,
tokenCredentialRequestWithNewGroup := &loginv1alpha1.TokenCredentialRequest{ authenticatorAPIGroup(authv1alpha1.SchemeGroupVersion.Group),
TypeMeta: metav1.TypeMeta{ )
APIVersion: replaceGV(t, loginv1alpha1.SchemeGroupVersion, newSuffix), tokenCredentialRequestWithCustomAPIGroupAuthenticator := with(
Kind: "TokenCredentialRequest", tokenCredentialRequest,
}, authenticatorAPIGroup(replaceGV(t, authv1alpha1.SchemeGroupVersion, newSuffix).Group),
} )
tokenCredentialRequestWithNewGroup := with(
tokenCredentialRequest,
gvk(replaceGV(t, loginv1alpha1.SchemeGroupVersion, newSuffix).WithKind("TokenCredentialRequest")),
)
tokenCredentialRequestWithNewGroupAndPinnipedAuthenticator := with(
tokenCredentialRequestWithNewGroup,
authenticatorAPIGroup(authv1alpha1.SchemeGroupVersion.Group),
)
tokenCredentialRequestWithNewGroupAndCustomAPIGroupAuthenticator := with(
tokenCredentialRequestWithNewGroup,
authenticatorAPIGroup(replaceGV(t, authv1alpha1.SchemeGroupVersion, newSuffix).Group),
)
tests := []struct { tests := []struct {
name string name string
apiGroupSuffix string apiGroupSuffix string
rt *testutil.RoundTrip rt *testutil.RoundTrip
requestObj, responseObj kubeclient.Object requestObj, responseObj kubeclient.Object
wantNilMiddleware bool wantNilMiddleware bool
wantMutateRequests, wantMutateResponses int wantMutateRequests, wantMutateResponses int
wantRequestObj, wantResponseObj kubeclient.Object wantMutateRequestErrors, wantMutateResponseErrors []string
wantRequestObj, wantResponseObj kubeclient.Object
skip bool
}{ }{
{ {
name: "api group suffix is empty", name: "api group suffix is empty",
@ -227,12 +240,12 @@ func TestMiddlware(t *testing.T) {
rt: (&testutil.RoundTrip{}). rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbGet). WithVerb(kubeclient.VerbGet).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")), WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest, requestObj: with(&metav1.PartialObjectMetadata{}, gvk(loginv1alpha1.SchemeGroupVersion.WithKind("TokenCredentialRequest"))),
responseObj: tokenCredentialRequest, responseObj: tokenCredentialRequestWithNewGroup,
wantMutateRequests: 1, wantMutateRequests: 1,
wantMutateResponses: 1, wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup, wantRequestObj: with(&metav1.PartialObjectMetadata{}, gvk(replaceGV(t, loginv1alpha1.SchemeGroupVersion, newSuffix).WithKind("TokenCredentialRequest"))),
wantResponseObj: tokenCredentialRequest, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
}, },
{ {
name: "create resource without pinniped.dev and without owner ref", name: "create resource without pinniped.dev and without owner ref",
@ -289,7 +302,7 @@ func TestMiddlware(t *testing.T) {
wantResponseObj: podWithPinnipedOwner, wantResponseObj: podWithPinnipedOwner,
}, },
{ {
// test that both of our middleware request mutations play nicely with each other // test that multiple of our middleware request mutations play nicely with each other
name: "create resource with pinniped.dev and with owner ref that has pinniped.dev owner", name: "create resource with pinniped.dev and with owner ref that has pinniped.dev owner",
apiGroupSuffix: newSuffix, apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}). rt: (&testutil.RoundTrip{}).
@ -297,11 +310,11 @@ func TestMiddlware(t *testing.T) {
WithNamespace("some-namespace"). WithNamespace("some-namespace").
WithResource(configv1alpha1.SchemeGroupVersion.WithResource("federationdomains")), WithResource(configv1alpha1.SchemeGroupVersion.WithResource("federationdomains")),
requestObj: federationDomainWithPinnipedOwner, requestObj: federationDomainWithPinnipedOwner,
responseObj: federationDomainWithPinnipedOwnerWithNewGroup, responseObj: federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup,
wantMutateRequests: 2, wantMutateRequests: 2,
wantMutateResponses: 1, wantMutateResponses: 1,
wantRequestObj: federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup, wantRequestObj: federationDomainWithNewGroupAndPinnipedOwnerWithNewGroup,
wantResponseObj: federationDomainWithPinnipedOwner, wantResponseObj: federationDomainWithNewGroupAndPinnipedOwner, // the middleware will reset object GVK for us
}, },
{ {
name: "update resource without pinniped.dev", name: "update resource without pinniped.dev",
@ -321,11 +334,11 @@ func TestMiddlware(t *testing.T) {
WithVerb(kubeclient.VerbUpdate). WithVerb(kubeclient.VerbUpdate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")), WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest, requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest, responseObj: tokenCredentialRequestWithNewGroup,
wantMutateRequests: 1, wantMutateRequests: 1,
wantMutateResponses: 1, wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup, wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
}, },
{ {
name: "list resource without pinniped.dev", name: "list resource without pinniped.dev",
@ -345,11 +358,11 @@ func TestMiddlware(t *testing.T) {
WithVerb(kubeclient.VerbList). WithVerb(kubeclient.VerbList).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")), WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest, requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest, responseObj: tokenCredentialRequestWithNewGroup,
wantMutateRequests: 1, wantMutateRequests: 1,
wantMutateResponses: 1, wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup, wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
}, },
{ {
name: "watch resource without pinniped.dev", name: "watch resource without pinniped.dev",
@ -369,11 +382,11 @@ func TestMiddlware(t *testing.T) {
WithVerb(kubeclient.VerbList). WithVerb(kubeclient.VerbList).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")), WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest, requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest, responseObj: tokenCredentialRequestWithNewGroup,
wantMutateRequests: 1, wantMutateRequests: 1,
wantMutateResponses: 1, wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup, wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
}, },
{ {
name: "patch resource without pinniped.dev", name: "patch resource without pinniped.dev",
@ -390,23 +403,98 @@ func TestMiddlware(t *testing.T) {
name: "patch resource with pinniped.dev", name: "patch resource with pinniped.dev",
apiGroupSuffix: newSuffix, apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}). rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbList). WithVerb(kubeclient.VerbPatch).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")), WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest, requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequest, responseObj: tokenCredentialRequestWithNewGroup,
wantMutateRequests: 1, wantMutateRequests: 1,
wantMutateResponses: 1, wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroup, wantRequestObj: tokenCredentialRequestWithNewGroup,
wantResponseObj: tokenCredentialRequest, wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
},
{
name: "create tokencredentialrequest with pinniped.dev authenticator api group",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequestWithPinnipedAuthenticator,
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 3,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroupAndCustomAPIGroupAuthenticator,
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
},
{
name: "create tokencredentialrequest with custom authenticator api group fails because api group is expected to be pinniped.dev",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequestWithCustomAPIGroupAuthenticator,
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 3,
wantMutateResponses: 1,
wantMutateRequestErrors: []string{`cannot replace token credential request "/" authenticator API group "authentication.concierge.some.suffix.com" with group suffix "some.suffix.com"`},
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
},
{
name: "create tokencredentialrequest with pinniped.dev authenticator api group and subresource",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")).
WithSubresource("some-subresource"),
requestObj: tokenCredentialRequestWithPinnipedAuthenticator,
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroupAndPinnipedAuthenticator,
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
},
{
name: "non-create tokencredentialrequest with pinniped.dev authenticator api group",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbList).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequestWithPinnipedAuthenticator,
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 1,
wantMutateResponses: 1,
wantRequestObj: tokenCredentialRequestWithNewGroupAndPinnipedAuthenticator,
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
},
{
name: "create tokencredentialrequest with nil authenticator api group",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: tokenCredentialRequest,
responseObj: tokenCredentialRequestWithNewGroup, // a token credential response does not contain a spec
wantMutateRequests: 3,
wantMutateResponses: 1,
wantMutateRequestErrors: []string{`cannot replace token credential request "/" without authenticator API group`},
wantResponseObj: tokenCredentialRequestWithNewGroup, // the middleware will reset object GVK for us
},
{
name: "create tokencredentialrequest with non-*loginv1alpha1.TokenCredentialRequest",
apiGroupSuffix: newSuffix,
rt: (&testutil.RoundTrip{}).
WithVerb(kubeclient.VerbCreate).
WithResource(loginv1alpha1.SchemeGroupVersion.WithResource("tokencredentialrequests")),
requestObj: podWithoutOwner,
responseObj: podWithoutOwner,
wantMutateRequests: 3,
wantMutateResponses: 1,
wantMutateRequestErrors: []string{`cannot cast obj of type *v1.Pod to *loginv1alpha1.TokenCredentialRequest`},
wantResponseObj: podWithoutOwner,
}, },
} }
for _, test := range tests { for _, test := range tests {
test := test test := test
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
if test.skip {
t.Skip()
}
m := New(test.apiGroupSuffix) m := New(test.apiGroupSuffix)
if test.wantNilMiddleware { if test.wantNilMiddleware {
require.Nil(t, m, "wanted nil middleware") require.Nil(t, m, "wanted nil middleware")
@ -414,27 +502,41 @@ func TestMiddlware(t *testing.T) {
} }
m.Handle(context.Background(), test.rt) m.Handle(context.Background(), test.rt)
require.Len(t, test.rt.MutateRequests, test.wantMutateRequests) require.Len(t, test.rt.MutateRequests, test.wantMutateRequests, "undesired request mutation count")
require.Len(t, test.rt.MutateResponses, test.wantMutateResponses) require.Len(t, test.rt.MutateResponses, test.wantMutateResponses, "undesired response mutation count")
if test.wantMutateRequests != 0 { if test.wantMutateRequests != 0 {
require.NotNil(t, test.requestObj, "expected test.requestObj to be set") require.NotNil(t, test.requestObj, "expected test.requestObj to be set")
objMutated := test.requestObj.DeepCopyObject().(kubeclient.Object) objMutated := test.requestObj.DeepCopyObject().(kubeclient.Object)
var mutateRequestErrors []string
for _, mutateRequest := range test.rt.MutateRequests { for _, mutateRequest := range test.rt.MutateRequests {
mutateRequest := mutateRequest mutateRequest := mutateRequest
mutateRequest(objMutated) if err := mutateRequest(objMutated); err != nil {
mutateRequestErrors = append(mutateRequestErrors, err.Error())
}
}
if len(test.wantMutateRequestErrors) > 0 {
require.Equal(t, test.wantMutateRequestErrors, mutateRequestErrors, "mutate request errors did not match")
} else {
require.Equal(t, test.wantRequestObj, objMutated, "request obj did not match")
} }
require.Equal(t, test.wantRequestObj, objMutated, "request obj did not match")
} }
if test.wantMutateResponses != 0 { if test.wantMutateResponses != 0 {
require.NotNil(t, test.responseObj, "expected test.responseObj to be set") require.NotNil(t, test.responseObj, "expected test.responseObj to be set")
objMutated := test.responseObj.DeepCopyObject().(kubeclient.Object) objMutated := test.responseObj.DeepCopyObject().(kubeclient.Object)
var mutateResponseErrors []string
for _, mutateResponse := range test.rt.MutateResponses { for _, mutateResponse := range test.rt.MutateResponses {
mutateResponse := mutateResponse mutateResponse := mutateResponse
mutateResponse(objMutated) if err := mutateResponse(objMutated); err != nil {
mutateResponseErrors = append(mutateResponseErrors, err.Error())
}
}
if len(test.wantMutateRequestErrors) > 0 {
require.Equal(t, test.wantMutateResponseErrors, mutateResponseErrors, "mutate response errors did not match")
} else {
require.Equal(t, test.wantResponseObj, objMutated, "response obj did not match")
} }
require.Equal(t, test.wantResponseObj, objMutated, "response obj did not match")
} }
}) })
} }
@ -519,11 +621,35 @@ func TestValidate(t *testing.T) {
} }
} }
func replaceGV(t *testing.T, baseGV schema.GroupVersion, apiGroupSuffix string) string { type withFunc func(obj kubeclient.Object)
func with(obj kubeclient.Object, withFuncs ...withFunc) kubeclient.Object {
obj = obj.DeepCopyObject().(kubeclient.Object)
for _, withFunc := range withFuncs {
withFunc(obj)
}
return obj
}
func gvk(gvk schema.GroupVersionKind) withFunc {
return func(obj kubeclient.Object) {
obj.GetObjectKind().SetGroupVersionKind(gvk)
}
}
func authenticatorAPIGroup(apiGroup string) withFunc {
return func(obj kubeclient.Object) {
tokenCredentialRequest := obj.(*loginv1alpha1.TokenCredentialRequest)
tokenCredentialRequest.Spec.Authenticator.APIGroup = &apiGroup
}
}
//nolint:unparam // the apiGroupSuffix parameter might always be the same, but this is nice for test readability
func replaceGV(t *testing.T, baseGV schema.GroupVersion, apiGroupSuffix string) schema.GroupVersion {
t.Helper() t.Helper()
groupName, ok := Replace(baseGV.Group, apiGroupSuffix) groupName, ok := Replace(baseGV.Group, apiGroupSuffix)
require.True(t, ok, "expected to be able to replace %q's suffix with %q", baseGV.Group, apiGroupSuffix) require.True(t, ok, "expected to be able to replace %q's suffix with %q", baseGV.Group, apiGroupSuffix)
return schema.GroupVersion{Group: groupName, Version: baseGV.Version}.String() return schema.GroupVersion{Group: groupName, Version: baseGV.Version}
} }
func chars(count int) string { func chars(count int) string {

View File

@ -492,8 +492,9 @@ func TestKubeclient(t *testing.T) {
return []*spyMiddleware{{ return []*spyMiddleware{{
name: "non-pertinent mutater", name: "non-pertinent mutater",
t: t, t: t,
mutateReq: func(rt RoundTrip, obj Object) { mutateReq: func(rt RoundTrip, obj Object) error {
clusterName()(obj) clusterName()(obj)
return nil
}, },
}} }}
}, },
@ -529,6 +530,64 @@ func TestKubeclient(t *testing.T) {
wantMiddlewareReqs: [][]Object{{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))}}, wantMiddlewareReqs: [][]Object{{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))}},
wantMiddlewareResps: [][]Object{nil}, wantMiddlewareResps: [][]Object{nil},
}, },
{
name: "when there are request middleware failures, we return an error and don't send the request",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{
// use 3 middleware to ensure that we collect all errors from all middlewares
newFailingMiddleware(t, "aaa", true, false),
newFailingMiddleware(t, "bbb", false, false),
newFailingMiddleware(t, "ccc", true, false),
}
},
reallyRunTest: func(t *testing.T, c *Client) {
_, err := c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Get(context.Background(), goodFederationDomain.Name, metav1.GetOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), ": request mutation failed: [aaa: request error, ccc: request error]")
},
wantMiddlewareReqs: [][]Object{
{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))},
{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))},
{with(&metav1.PartialObjectMetadata{}, gvk(federationDomainGVK))},
},
wantMiddlewareResps: [][]Object{
nil,
nil,
nil,
},
},
{
name: "when there are response middleware failures, we return an error",
middlewares: func(t *testing.T) []*spyMiddleware {
return []*spyMiddleware{
// use 3 middleware to ensure that we collect all errors from all middlewares
newFailingMiddleware(t, "aaa", false, true),
newFailingMiddleware(t, "bbb", false, false),
newFailingMiddleware(t, "ccc", false, true),
}
},
reallyRunTest: func(t *testing.T, c *Client) {
_, err := c.PinnipedSupervisor.
ConfigV1alpha1().
FederationDomains(goodFederationDomain.Namespace).
Create(context.Background(), goodFederationDomain, metav1.CreateOptions{})
require.Error(t, err)
require.Contains(t, err.Error(), ": response mutation failed: [aaa: response error, ccc: response error]")
},
wantMiddlewareReqs: [][]Object{
{with(goodFederationDomain, gvk(federationDomainGVK))},
{with(goodFederationDomain, gvk(federationDomainGVK))},
{with(goodFederationDomain, gvk(federationDomainGVK))},
},
wantMiddlewareResps: [][]Object{
{with(goodFederationDomain, gvk(federationDomainGVK))},
{with(goodFederationDomain, gvk(federationDomainGVK))},
{with(goodFederationDomain, gvk(federationDomainGVK))},
},
},
} }
for _, test := range tests { for _, test := range tests {
test := test test := test
@ -568,8 +627,8 @@ func TestKubeclient(t *testing.T) {
type spyMiddleware struct { type spyMiddleware struct {
name string name string
t *testing.T t *testing.T
mutateReq func(RoundTrip, Object) mutateReq func(RoundTrip, Object) error
mutateResp func(RoundTrip, Object) mutateResp func(RoundTrip, Object) error
reqObjs []Object reqObjs []Object
respObjs []Object respObjs []Object
} }
@ -578,18 +637,18 @@ func (s *spyMiddleware) Handle(_ context.Context, rt RoundTrip) {
s.t.Log(s.name, "handling", reqStr(rt, nil)) s.t.Log(s.name, "handling", reqStr(rt, nil))
if s.mutateReq != nil { if s.mutateReq != nil {
rt.MutateRequest(func(obj Object) { rt.MutateRequest(func(obj Object) error {
s.t.Log(s.name, "mutating request", reqStr(rt, obj)) s.t.Log(s.name, "mutating request", reqStr(rt, obj))
s.reqObjs = append(s.reqObjs, obj.DeepCopyObject().(Object)) s.reqObjs = append(s.reqObjs, obj.DeepCopyObject().(Object))
s.mutateReq(rt, obj) return s.mutateReq(rt, obj)
}) })
} }
if s.mutateResp != nil { if s.mutateResp != nil {
rt.MutateResponse(func(obj Object) { rt.MutateResponse(func(obj Object) error {
s.t.Log(s.name, "mutating response", reqStr(rt, obj)) s.t.Log(s.name, "mutating response", reqStr(rt, obj))
s.respObjs = append(s.respObjs, obj.DeepCopyObject().(Object)) s.respObjs = append(s.respObjs, obj.DeepCopyObject().(Object))
s.mutateResp(rt, obj) return s.mutateResp(rt, obj)
}) })
} }
} }
@ -611,17 +670,19 @@ func newAnnotationMiddleware(t *testing.T) *spyMiddleware {
return &spyMiddleware{ return &spyMiddleware{
name: "annotater", name: "annotater",
t: t, t: t,
mutateReq: func(rt RoundTrip, obj Object) { mutateReq: func(rt RoundTrip, obj Object) error {
if rt.Verb() == VerbCreate { if rt.Verb() == VerbCreate {
annotations()(obj) annotations()(obj)
} }
return nil
}, },
mutateResp: func(rt RoundTrip, obj Object) { mutateResp: func(rt RoundTrip, obj Object) error {
if rt.Verb() == VerbCreate { if rt.Verb() == VerbCreate {
for key := range middlewareAnnotations { for key := range middlewareAnnotations {
delete(obj.GetAnnotations(), key) delete(obj.GetAnnotations(), key)
} }
} }
return nil
}, },
} }
} }
@ -630,41 +691,69 @@ func newLabelMiddleware(t *testing.T) *spyMiddleware {
return &spyMiddleware{ return &spyMiddleware{
name: "labeler", name: "labeler",
t: t, t: t,
mutateReq: func(rt RoundTrip, obj Object) { mutateReq: func(rt RoundTrip, obj Object) error {
if rt.Verb() == VerbCreate { if rt.Verb() == VerbCreate {
labels()(obj) labels()(obj)
} }
return nil
}, },
mutateResp: func(rt RoundTrip, obj Object) { mutateResp: func(rt RoundTrip, obj Object) error {
if rt.Verb() == VerbCreate { if rt.Verb() == VerbCreate {
for key := range middlewareLabels { for key := range middlewareLabels {
delete(obj.GetLabels(), key) delete(obj.GetLabels(), key)
} }
} }
return nil
}, },
} }
} }
func newSimpleMiddleware(t *testing.T, hasMutateReqFunc, mutatedReq, hasMutateRespFunc bool) *spyMiddleware { func newSimpleMiddleware(t *testing.T, hasMutateReqFunc, mutatedReq, hasMutateRespFunc bool) *spyMiddleware {
m := &spyMiddleware{ m := &spyMiddleware{
name: "nop", name: "simple",
t: t, t: t,
} }
if hasMutateReqFunc { if hasMutateReqFunc {
m.mutateReq = func(rt RoundTrip, obj Object) { m.mutateReq = func(rt RoundTrip, obj Object) error {
if mutatedReq { if mutatedReq {
if rt.Verb() == VerbCreate { if rt.Verb() == VerbCreate {
obj.SetClusterName(someClusterName) obj.SetClusterName(someClusterName)
} }
} }
return nil
} }
} }
if hasMutateRespFunc { if hasMutateRespFunc {
m.mutateResp = func(rt RoundTrip, obj Object) {} m.mutateResp = func(rt RoundTrip, obj Object) error {
return nil
}
} }
return m return m
} }
func newFailingMiddleware(t *testing.T, name string, mutateReqFails, mutateRespFails bool) *spyMiddleware {
m := &spyMiddleware{
name: "failing-middleware-" + name,
t: t,
}
m.mutateReq = func(rt RoundTrip, obj Object) error {
if mutateReqFails {
return fmt.Errorf("%s: request error", name)
}
return nil
}
m.mutateResp = func(rt RoundTrip, obj Object) error {
if mutateRespFails {
return fmt.Errorf("%s: response error", name)
}
return nil
}
return m
}
type wantCloser struct { type wantCloser struct {
io.ReadCloser io.ReadCloser
closeCount int closeCount int

View File

@ -12,6 +12,7 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/errors"
) )
type Middleware interface { type Middleware interface {
@ -43,8 +44,8 @@ type RoundTrip interface {
NamespaceScoped() bool NamespaceScoped() bool
Resource() schema.GroupVersionResource Resource() schema.GroupVersionResource
Subresource() string Subresource() string
MutateRequest(f func(obj Object)) MutateRequest(f func(obj Object) error)
MutateResponse(f func(obj Object)) MutateResponse(f func(obj Object) error)
} }
type Object interface { type Object interface {
@ -58,7 +59,7 @@ type request struct {
verb Verb verb Verb
namespace string namespace string
resource schema.GroupVersionResource resource schema.GroupVersionResource
reqFuncs, respFuncs []func(obj Object) reqFuncs, respFuncs []func(obj Object) error
subresource string subresource string
} }
@ -89,11 +90,11 @@ func (r *request) Subresource() string {
return r.subresource return r.subresource
} }
func (r *request) MutateRequest(f func(obj Object)) { func (r *request) MutateRequest(f func(obj Object) error) {
r.reqFuncs = append(r.reqFuncs, f) r.reqFuncs = append(r.reqFuncs, f)
} }
func (r *request) MutateResponse(f func(obj Object)) { func (r *request) MutateResponse(f func(obj Object) error) {
r.respFuncs = append(r.respFuncs, f) r.respFuncs = append(r.respFuncs, f)
} }
@ -113,9 +114,15 @@ func (r *request) mutateRequest(obj Object) (*mutationResult, error) {
return nil, fmt.Errorf("invalid deep copy semantics for %T: %#v", obj, r) return nil, fmt.Errorf("invalid deep copy semantics for %T: %#v", obj, r)
} }
var errs []error
for _, reqFunc := range r.reqFuncs { for _, reqFunc := range r.reqFuncs {
reqFunc := reqFunc reqFunc := reqFunc
reqFunc(obj) if err := reqFunc(obj); err != nil {
errs = append(errs, err)
}
}
if err := errors.NewAggregate(errs); err != nil {
return nil, fmt.Errorf("request mutation failed: %w", err)
} }
newGVK := obj.GetObjectKind().GroupVersionKind() newGVK := obj.GetObjectKind().GroupVersionKind()
@ -137,9 +144,15 @@ func (r *request) mutateResponse(obj Object) (bool, error) {
return false, fmt.Errorf("invalid deep copy semantics for %T: %#v", obj, r) return false, fmt.Errorf("invalid deep copy semantics for %T: %#v", obj, r)
} }
var errs []error
for _, respFunc := range r.respFuncs { for _, respFunc := range r.respFuncs {
respFunc := respFunc respFunc := respFunc
respFunc(obj) if err := respFunc(obj); err != nil {
errs = append(errs, err)
}
}
if err := errors.NewAggregate(errs); err != nil {
return false, fmt.Errorf("response mutation failed: %w", err)
} }
mutated := !apiequality.Semantic.DeepEqual(origObj, obj) mutated := !apiequality.Semantic.DeepEqual(origObj, obj)

View File

@ -15,7 +15,7 @@ import (
func Test_request_mutate(t *testing.T) { func Test_request_mutate(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
reqFuncs []func(Object) reqFuncs []func(Object) error
obj Object obj Object
want *mutationResult want *mutationResult
wantObj Object wantObj Object
@ -23,10 +23,11 @@ func Test_request_mutate(t *testing.T) {
}{ }{
{ {
name: "mutate config map data", name: "mutate config map data",
reqFuncs: []func(Object){ reqFuncs: []func(Object) error{
func(obj Object) { func(obj Object) error {
cm := obj.(*corev1.ConfigMap) cm := obj.(*corev1.ConfigMap)
cm.Data = map[string]string{"new": "stuff"} cm.Data = map[string]string{"new": "stuff"}
return nil
}, },
}, },
obj: &corev1.ConfigMap{ obj: &corev1.ConfigMap{

View File

@ -42,7 +42,7 @@ func Test_verb(t *testing.T) {
f: func() string { f: func() string {
return fmt.Errorf("%#v", request{verb: VerbPatch}).Error() return fmt.Errorf("%#v", request{verb: VerbPatch}).Error()
}, },
want: `kubeclient.request{verb:"patch", namespace:"", resource:schema.GroupVersionResource{Group:"", Version:"", Resource:""}, reqFuncs:[]func(kubeclient.Object)(nil), respFuncs:[]func(kubeclient.Object)(nil), subresource:""}`, want: `kubeclient.request{verb:"patch", namespace:"", resource:schema.GroupVersionResource{Group:"", Version:"", Resource:""}, reqFuncs:[]func(kubeclient.Object) error(nil), respFuncs:[]func(kubeclient.Object) error(nil), subresource:""}`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View File

@ -51,13 +51,15 @@ func New(refObj kubeclient.Object) kubeclient.Middleware {
return return
} }
rt.MutateRequest(func(obj kubeclient.Object) { rt.MutateRequest(func(obj kubeclient.Object) error {
// we only want to set the owner ref on create and when one is not already present // we only want to set the owner ref on create and when one is not already present
if len(obj.GetOwnerReferences()) != 0 { if len(obj.GetOwnerReferences()) != 0 {
return return nil
} }
obj.SetOwnerReferences([]metav1.OwnerReference{ref}) obj.SetOwnerReferences([]metav1.OwnerReference{ref})
return nil
}) })
}) })
} }

View File

@ -213,7 +213,7 @@ func TestOwnerReferenceMiddleware(t *testing.T) {
orig := tt.args.obj.DeepCopyObject().(kubeclient.Object) orig := tt.args.obj.DeepCopyObject().(kubeclient.Object)
for _, mutateRequest := range rt.MutateRequests { for _, mutateRequest := range rt.MutateRequests {
mutateRequest := mutateRequest mutateRequest := mutateRequest
mutateRequest(tt.args.obj) require.NoError(t, mutateRequest(tt.args.obj))
} }
if !tt.wantMutates { if !tt.wantMutates {
require.Equal(t, orig, tt.args.obj) require.Equal(t, orig, tt.args.obj)

View File

@ -17,7 +17,7 @@ type RoundTrip struct {
resource schema.GroupVersionResource resource schema.GroupVersionResource
subresource string subresource string
MutateRequests, MutateResponses []func(kubeclient.Object) MutateRequests, MutateResponses []func(kubeclient.Object) error
} }
func (rt *RoundTrip) WithVerb(verb kubeclient.Verb) *RoundTrip { func (rt *RoundTrip) WithVerb(verb kubeclient.Verb) *RoundTrip {
@ -61,10 +61,10 @@ func (rt *RoundTrip) Subresource() string {
return rt.subresource return rt.subresource
} }
func (rt *RoundTrip) MutateRequest(fn func(kubeclient.Object)) { func (rt *RoundTrip) MutateRequest(fn func(kubeclient.Object) error) {
rt.MutateRequests = append(rt.MutateRequests, fn) rt.MutateRequests = append(rt.MutateRequests, fn)
} }
func (rt *RoundTrip) MutateResponse(fn func(kubeclient.Object)) { func (rt *RoundTrip) MutateResponse(fn func(kubeclient.Object) error) {
rt.MutateResponses = append(rt.MutateResponses, fn) rt.MutateResponses = append(rt.MutateResponses, fn)
} }

View File

@ -12,7 +12,7 @@ import (
"net/url" "net/url"
"strings" "strings"
v1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1" clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
"k8s.io/client-go/tools/clientcmd" "k8s.io/client-go/tools/clientcmd"
@ -34,12 +34,11 @@ type Option func(*Client) error
// Client is a configuration for talking to the Pinniped concierge. // Client is a configuration for talking to the Pinniped concierge.
type Client struct { type Client struct {
namespace string namespace string
authenticatorName string authenticator *corev1.TypedLocalObjectReference
authenticatorKind string caBundle string
caBundle string endpoint *url.URL
endpoint *url.URL apiGroupSuffix string
apiGroupSuffix string
} }
// WithNamespace configures the namespace where the TokenCredentialRequest is to be sent. // WithNamespace configures the namespace where the TokenCredentialRequest is to be sent.
@ -56,15 +55,18 @@ func WithAuthenticator(authType, authName string) Option {
if authName == "" { if authName == "" {
return fmt.Errorf("authenticator name must not be empty") return fmt.Errorf("authenticator name must not be empty")
} }
c.authenticatorName = authName authenticator := corev1.TypedLocalObjectReference{Name: authName}
switch strings.ToLower(authType) { switch strings.ToLower(authType) {
case "webhook": case "webhook":
c.authenticatorKind = "WebhookAuthenticator" authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
authenticator.Kind = "WebhookAuthenticator"
case "jwt": case "jwt":
c.authenticatorKind = "JWTAuthenticator" authenticator.APIGroup = &auth1alpha1.SchemeGroupVersion.Group
authenticator.Kind = "JWTAuthenticator"
default: default:
return fmt.Errorf(`invalid authenticator type: %q, supported values are "webhook" and "jwt"`, authType) return fmt.Errorf(`invalid authenticator type: %q, supported values are "webhook" and "jwt"`, authType)
} }
c.authenticator = &authenticator
return nil return nil
} }
} }
@ -131,7 +133,7 @@ func New(opts ...Option) (*Client, error) {
return nil, err return nil, err
} }
} }
if c.authenticatorName == "" { if c.authenticator == nil {
return nil, fmt.Errorf("WithAuthenticator must be specified") return nil, fmt.Errorf("WithAuthenticator must be specified")
} }
if c.endpoint == nil { if c.endpoint == nil {
@ -178,18 +180,13 @@ func (c *Client) ExchangeToken(ctx context.Context, token string) (*clientauthen
if err != nil { if err != nil {
return nil, err return nil, err
} }
replacedAPIGroupName, _ := groupsuffix.Replace(auth1alpha1.SchemeGroupVersion.Group, c.apiGroupSuffix)
resp, err := clientset.LoginV1alpha1().TokenCredentialRequests(c.namespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{ resp, err := clientset.LoginV1alpha1().TokenCredentialRequests(c.namespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Namespace: c.namespace, Namespace: c.namespace,
}, },
Spec: loginv1alpha1.TokenCredentialRequestSpec{ Spec: loginv1alpha1.TokenCredentialRequestSpec{
Token: token, Token: token,
Authenticator: v1.TypedLocalObjectReference{ Authenticator: *c.authenticator,
APIGroup: &replacedAPIGroupName,
Kind: c.authenticatorKind,
Name: c.authenticatorName,
},
}, },
}, metav1.CreateOptions{}) }, metav1.CreateOptions{})
if err != nil { if err != nil {

View File

@ -21,7 +21,6 @@ import (
loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1" loginv1alpha1 "go.pinniped.dev/generated/1.20/apis/concierge/login/v1alpha1"
"go.pinniped.dev/internal/certauthority" "go.pinniped.dev/internal/certauthority"
"go.pinniped.dev/internal/here"
"go.pinniped.dev/internal/testutil" "go.pinniped.dev/internal/testutil"
) )
@ -221,7 +220,47 @@ func TestExchangeToken(t *testing.T) {
t.Parallel() t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second)) expires := metav1.NewTime(time.Now().Truncate(time.Second))
caBundle, endpoint := runFakeServer(t, expires, "pinniped.dev") // Start a test server that returns successfully and asserts various properties of the request.
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t, "/apis/login.concierge.pinniped.dev/v1alpha1/namespaces/test-namespace/tokencredentialrequests", r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t,
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.pinniped.dev/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.pinniped.dev",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`,
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{APIVersion: "login.concierge.pinniped.dev/v1alpha1", Kind: "TokenCredentialRequest"},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
client, err := New(WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook")) client, err := New(WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
require.NoError(t, err) require.NoError(t, err)
@ -240,78 +279,4 @@ func TestExchangeToken(t *testing.T) {
}, },
}, got) }, got)
}) })
t.Run("changing the API group suffix for the client sends the custom suffix on the CredentialRequest's APIGroup and on its spec.Authenticator.APIGroup", func(t *testing.T) {
t.Parallel()
expires := metav1.NewTime(time.Now().Truncate(time.Second))
caBundle, endpoint := runFakeServer(t, expires, "suffix.com")
client, err := New(WithAPIGroupSuffix("suffix.com"), WithNamespace("test-namespace"), WithEndpoint(endpoint), WithCABundle(caBundle), WithAuthenticator("webhook", "test-webhook"))
require.NoError(t, err)
got, err := client.ExchangeToken(ctx, "test-token")
require.NoError(t, err)
require.Equal(t, &clientauthenticationv1beta1.ExecCredential{
TypeMeta: metav1.TypeMeta{
Kind: "ExecCredential",
APIVersion: "client.authentication.k8s.io/v1beta1",
},
Status: &clientauthenticationv1beta1.ExecCredentialStatus{
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
ExpirationTimestamp: &expires,
},
}, got)
})
}
// Start a test server that returns successfully and asserts various properties of the request.
func runFakeServer(t *testing.T, expires metav1.Time, pinnipedAPIGroupSuffix string) (string, string) {
caBundle, endpoint := testutil.TLSTestServer(t, func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, http.MethodPost, r.Method)
require.Equal(t,
fmt.Sprintf("/apis/login.concierge.%s/v1alpha1/namespaces/test-namespace/tokencredentialrequests", pinnipedAPIGroupSuffix),
r.URL.Path)
require.Equal(t, "application/json", r.Header.Get("content-type"))
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
require.JSONEq(t, here.Docf(
`{
"kind": "TokenCredentialRequest",
"apiVersion": "login.concierge.%s/v1alpha1",
"metadata": {
"creationTimestamp": null,
"namespace": "test-namespace"
},
"spec": {
"token": "test-token",
"authenticator": {
"apiGroup": "authentication.concierge.%s",
"kind": "WebhookAuthenticator",
"name": "test-webhook"
}
},
"status": {}
}`, pinnipedAPIGroupSuffix, pinnipedAPIGroupSuffix),
string(body),
)
w.Header().Set("content-type", "application/json")
_ = json.NewEncoder(w).Encode(&loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{
APIVersion: fmt.Sprintf("login.concierge.%s/v1alpha1", pinnipedAPIGroupSuffix),
Kind: "TokenCredentialRequest",
},
Status: loginv1alpha1.TokenCredentialRequestStatus{
Credential: &loginv1alpha1.ClusterCredential{
ExpirationTimestamp: expires,
ClientCertificateData: "test-certificate",
ClientKeyData: "test-key",
},
},
})
})
return caBundle, endpoint
} }

View File

@ -78,11 +78,15 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
kubeClient := library.NewKubernetesClientset(t) kubeClient := library.NewKubernetesClientset(t)
aggregatedClient := library.NewAggregatedClientset(t) aggregatedClient := library.NewAggregatedClientset(t)
conciergeClient := library.NewConciergeClientset(t) conciergeClient := library.NewConciergeClientset(t)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
defer cancel() defer cancel()
apiServiceName := "v1alpha1.login.concierge." + env.APIGroupSuffix apiServiceName := "v1alpha1.login.concierge." + env.APIGroupSuffix
// Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
testWebhook := library.CreateTestWebhookAuthenticator(ctx, t)
// Get the initial auto-generated version of the Secret. // Get the initial auto-generated version of the Secret.
secret, err := kubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, defaultServingCertResourceName, metav1.GetOptions{}) secret, err := kubeClient.CoreV1().Secrets(env.ConciergeNamespace).Get(ctx, defaultServingCertResourceName, metav1.GetOptions{})
require.NoError(t, err) require.NoError(t, err)
@ -145,7 +149,7 @@ func TestAPIServingCertificateAutoCreationAndRotation(t *testing.T) {
_, err = conciergeClient.LoginV1alpha1().TokenCredentialRequests(env.ConciergeNamespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{ _, err = conciergeClient.LoginV1alpha1().TokenCredentialRequests(env.ConciergeNamespace).Create(ctx, &loginv1alpha1.TokenCredentialRequest{
TypeMeta: metav1.TypeMeta{}, TypeMeta: metav1.TypeMeta{},
ObjectMeta: metav1.ObjectMeta{}, ObjectMeta: metav1.ObjectMeta{},
Spec: loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token"}, Spec: loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token", Authenticator: testWebhook},
}, metav1.CreateOptions{}) }, metav1.CreateOptions{})
if err != nil { if err != nil {
break break

View File

@ -135,7 +135,16 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic
library.AssertNoRestartsDuringTest(t, env.ConciergeNamespace, "") library.AssertNoRestartsDuringTest(t, env.ConciergeNamespace, "")
response, err := makeRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token"}) // Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
testWebhook := library.CreateTestWebhookAuthenticator(ctx, t)
response, err := makeRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{
Token: "not a good token",
Authenticator: testWebhook,
})
require.NoError(t, err) require.NoError(t, err)
@ -149,7 +158,16 @@ func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken(t *testing.T
library.AssertNoRestartsDuringTest(t, env.ConciergeNamespace, "") library.AssertNoRestartsDuringTest(t, env.ConciergeNamespace, "")
response, err := makeRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{Token: ""}) // Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
defer cancel()
testWebhook := library.CreateTestWebhookAuthenticator(ctx, t)
response, err := makeRequest(context.Background(), t, loginv1alpha1.TokenCredentialRequestSpec{
Token: "",
Authenticator: testWebhook,
})
require.Error(t, err) require.Error(t, err)
statusError, isStatus := err.(*errors.StatusError) statusError, isStatus := err.(*errors.StatusError)

View File

@ -180,11 +180,8 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty
require.NoErrorf(t, err, "could not cleanup test WebhookAuthenticator %s/%s", webhook.Namespace, webhook.Name) require.NoErrorf(t, err, "could not cleanup test WebhookAuthenticator %s/%s", webhook.Namespace, webhook.Name)
}) })
apiGroup, replacedSuffix := groupsuffix.Replace(auth1alpha1.SchemeGroupVersion.Group, testEnv.APIGroupSuffix)
require.True(t, replacedSuffix)
return corev1.TypedLocalObjectReference{ return corev1.TypedLocalObjectReference{
APIGroup: &apiGroup, APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
Kind: "WebhookAuthenticator", Kind: "WebhookAuthenticator",
Name: webhook.Name, Name: webhook.Name,
} }
@ -253,11 +250,8 @@ func CreateTestJWTAuthenticator(ctx context.Context, t *testing.T, spec auth1alp
require.NoErrorf(t, err, "could not cleanup test JWTAuthenticator %s/%s", jwtAuthenticator.Namespace, jwtAuthenticator.Name) require.NoErrorf(t, err, "could not cleanup test JWTAuthenticator %s/%s", jwtAuthenticator.Namespace, jwtAuthenticator.Name)
}) })
apiGroup, replacedSuffix := groupsuffix.Replace(auth1alpha1.SchemeGroupVersion.Group, testEnv.APIGroupSuffix)
require.True(t, replacedSuffix)
return corev1.TypedLocalObjectReference{ return corev1.TypedLocalObjectReference{
APIGroup: &apiGroup, APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
Kind: "JWTAuthenticator", Kind: "JWTAuthenticator",
Name: jwtAuthenticator.Name, Name: jwtAuthenticator.Name,
} }