Add no-op list support to token credential request

This allows us to keep all of our resources in the pinniped category
while not having kubectl return errors for calls such as:

kubectl get pinniped -A

Signed-off-by: Monis Khan <mok@vmware.com>
This commit is contained in:
Monis Khan 2021-02-05 10:55:19 -05:00
parent ee05f155ca
commit f7958ae75b
No known key found for this signature in database
GPG Key ID: 52C90ADA01B269B8
5 changed files with 161 additions and 19 deletions

View File

@ -71,7 +71,7 @@ func (c completedConfig) New() (*PinnipedServer, error) {
} }
gvr := c.ExtraConfig.GroupVersion.WithResource("tokencredentialrequests") gvr := c.ExtraConfig.GroupVersion.WithResource("tokencredentialrequests")
storage := credentialrequest.NewREST(c.ExtraConfig.Authenticator, c.ExtraConfig.Issuer) storage := credentialrequest.NewREST(c.ExtraConfig.Authenticator, c.ExtraConfig.Issuer, gvr.GroupResource())
if err := s.GenericAPIServer.InstallAPIGroup(&genericapiserver.APIGroupInfo{ if err := s.GenericAPIServer.InstallAPIGroup(&genericapiserver.APIGroupInfo{
PrioritizedVersions: []schema.GroupVersion{gvr.GroupVersion()}, PrioritizedVersions: []schema.GroupVersion{gvr.GroupVersion()},
VersionedResourcesStorageMap: map[string]map[string]rest.Storage{gvr.Version: {gvr.Resource: storage}}, VersionedResourcesStorageMap: map[string]map[string]rest.Storage{gvr.Version: {gvr.Resource: storage}},

View File

@ -11,8 +11,10 @@ import (
"time" "time"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
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/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
@ -32,16 +34,18 @@ type TokenCredentialRequestAuthenticator interface {
AuthenticateTokenCredentialRequest(ctx context.Context, req *loginapi.TokenCredentialRequest) (user.Info, error) AuthenticateTokenCredentialRequest(ctx context.Context, req *loginapi.TokenCredentialRequest) (user.Info, error)
} }
func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer CertIssuer) *REST { func NewREST(authenticator TokenCredentialRequestAuthenticator, issuer CertIssuer, resource schema.GroupResource) *REST {
return &REST{ return &REST{
authenticator: authenticator, authenticator: authenticator,
issuer: issuer, issuer: issuer,
tableConvertor: rest.NewDefaultTableConvertor(resource),
} }
} }
type REST struct { type REST struct {
authenticator TokenCredentialRequestAuthenticator authenticator TokenCredentialRequestAuthenticator
issuer CertIssuer issuer CertIssuer
tableConvertor rest.TableConvertor
} }
// Assert that our *REST implements all the optional interfaces that we expect it to implement. // Assert that our *REST implements all the optional interfaces that we expect it to implement.
@ -51,12 +55,30 @@ var _ interface {
rest.Scoper rest.Scoper
rest.Storage rest.Storage
rest.CategoriesProvider rest.CategoriesProvider
rest.Lister
} = (*REST)(nil) } = (*REST)(nil)
func (*REST) New() runtime.Object { func (*REST) New() runtime.Object {
return &loginapi.TokenCredentialRequest{} return &loginapi.TokenCredentialRequest{}
} }
func (*REST) NewList() runtime.Object {
return &loginapi.TokenCredentialRequestList{}
}
func (*REST) List(_ context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) {
return &loginapi.TokenCredentialRequestList{
ListMeta: metav1.ListMeta{
ResourceVersion: "0", // this resource version means "from the API server cache"
},
Items: []loginapi.TokenCredentialRequest{}, // avoid sending nil items list
}, nil
}
func (r *REST) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return r.tableConvertor.ConvertToTable(ctx, obj, tableOptions)
}
func (*REST) NamespaceScoped() bool { func (*REST) NamespaceScoped() bool {
return true return true
} }

View File

@ -17,6 +17,7 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
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/apiserver/pkg/authentication/user" "k8s.io/apiserver/pkg/authentication/user"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request" genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest" "k8s.io/apiserver/pkg/registry/rest"
@ -28,11 +29,34 @@ import (
) )
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
r := NewREST(nil, nil) r := NewREST(nil, nil, schema.GroupResource{Group: "bears", Resource: "panda"})
require.NotNil(t, r) require.NotNil(t, r)
require.True(t, r.NamespaceScoped()) require.True(t, r.NamespaceScoped())
require.Equal(t, []string{"pinniped"}, r.Categories()) require.Equal(t, []string{"pinniped"}, r.Categories())
require.IsType(t, &loginapi.TokenCredentialRequest{}, r.New()) require.IsType(t, &loginapi.TokenCredentialRequest{}, r.New())
require.IsType(t, &loginapi.TokenCredentialRequestList{}, r.NewList())
ctx := context.Background()
// check the simple invariants of our no-op list
list, err := r.List(ctx, nil)
require.NoError(t, err)
require.NotNil(t, list)
require.IsType(t, &loginapi.TokenCredentialRequestList{}, list)
require.Equal(t, "0", list.(*loginapi.TokenCredentialRequestList).ResourceVersion)
require.NotNil(t, list.(*loginapi.TokenCredentialRequestList).Items)
require.Len(t, list.(*loginapi.TokenCredentialRequestList).Items, 0)
// make sure we can turn lists into tables if needed
table, err := r.ConvertToTable(ctx, list, nil)
require.NoError(t, err)
require.NotNil(t, table)
require.Equal(t, "0", table.ResourceVersion)
require.Nil(t, table.Rows)
// exercise group resource - force error by passing a runtime.Object that does not have an embedded object meta
_, err = r.ConvertToTable(ctx, &metav1.APIGroup{}, nil)
require.Error(t, err, "the resource panda.bears does not support being converted to a Table")
} }
func TestCreate(t *testing.T) { func TestCreate(t *testing.T) {
@ -73,7 +97,7 @@ func TestCreate(t *testing.T) {
5*time.Minute, 5*time.Minute,
).Return([]byte("test-cert"), []byte("test-key"), nil) ).Return([]byte("test-cert"), []byte("test-key"), nil)
storage := NewREST(requestAuthenticator, issuer) storage := NewREST(requestAuthenticator, issuer, schema.GroupResource{})
response, err := callCreate(context.Background(), storage, req) response, err := callCreate(context.Background(), storage, req)
@ -112,7 +136,7 @@ func TestCreate(t *testing.T) {
IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()). IssuePEM(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, nil, fmt.Errorf("some certificate authority error")) Return(nil, nil, fmt.Errorf("some certificate authority error"))
storage := NewREST(requestAuthenticator, issuer) storage := NewREST(requestAuthenticator, issuer, schema.GroupResource{})
response, err := callCreate(context.Background(), storage, req) response, err := callCreate(context.Background(), storage, req)
requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response) requireSuccessfulResponseWithAuthenticationFailureMessage(t, err, response)
@ -125,7 +149,7 @@ func TestCreate(t *testing.T) {
requestAuthenticator := credentialrequestmocks.NewMockTokenCredentialRequestAuthenticator(ctrl) requestAuthenticator := credentialrequestmocks.NewMockTokenCredentialRequestAuthenticator(ctrl)
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).Return(nil, nil) requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).Return(nil, nil)
storage := NewREST(requestAuthenticator, nil) storage := NewREST(requestAuthenticator, nil, schema.GroupResource{})
response, err := callCreate(context.Background(), storage, req) response, err := callCreate(context.Background(), storage, req)
@ -140,7 +164,7 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req). requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).
Return(nil, errors.New("some webhook error")) Return(nil, errors.New("some webhook error"))
storage := NewREST(requestAuthenticator, nil) storage := NewREST(requestAuthenticator, nil, schema.GroupResource{})
response, err := callCreate(context.Background(), storage, req) response, err := callCreate(context.Background(), storage, req)
@ -155,7 +179,7 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req). requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req).
Return(&user.DefaultInfo{Name: ""}, nil) Return(&user.DefaultInfo{Name: ""}, nil)
storage := NewREST(requestAuthenticator, nil) storage := NewREST(requestAuthenticator, nil, schema.GroupResource{})
response, err := callCreate(context.Background(), storage, req) response, err := callCreate(context.Background(), storage, req)
@ -165,7 +189,7 @@ func TestCreate(t *testing.T) {
it("CreateFailsWhenGivenTheWrongInputType", func() { it("CreateFailsWhenGivenTheWrongInputType", func() {
notACredentialRequest := runtime.Unknown{} notACredentialRequest := runtime.Unknown{}
response, err := NewREST(nil, nil).Create( response, err := NewREST(nil, nil, schema.GroupResource{}).Create(
genericapirequest.NewContext(), genericapirequest.NewContext(),
&notACredentialRequest, &notACredentialRequest,
rest.ValidateAllObjectFunc, rest.ValidateAllObjectFunc,
@ -176,7 +200,7 @@ func TestCreate(t *testing.T) {
}) })
it("CreateFailsWhenTokenValueIsEmptyInRequest", func() { it("CreateFailsWhenTokenValueIsEmptyInRequest", func() {
storage := NewREST(nil, nil) storage := NewREST(nil, nil, schema.GroupResource{})
response, err := callCreate(context.Background(), storage, credentialRequest(loginapi.TokenCredentialRequestSpec{ response, err := callCreate(context.Background(), storage, credentialRequest(loginapi.TokenCredentialRequestSpec{
Token: "", Token: "",
})) }))
@ -187,7 +211,7 @@ func TestCreate(t *testing.T) {
}) })
it("CreateFailsWhenValidationFails", func() { it("CreateFailsWhenValidationFails", func() {
storage := NewREST(nil, nil) storage := NewREST(nil, nil, schema.GroupResource{})
response, err := storage.Create( response, err := storage.Create(
context.Background(), context.Background(),
validCredentialRequest(), validCredentialRequest(),
@ -207,7 +231,7 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req.DeepCopy()). requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req.DeepCopy()).
Return(&user.DefaultInfo{Name: "test-user"}, nil) Return(&user.DefaultInfo{Name: "test-user"}, nil)
storage := NewREST(requestAuthenticator, successfulIssuer(ctrl)) storage := NewREST(requestAuthenticator, successfulIssuer(ctrl), schema.GroupResource{})
response, err := storage.Create( response, err := storage.Create(
context.Background(), context.Background(),
req, req,
@ -228,7 +252,7 @@ func TestCreate(t *testing.T) {
requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req.DeepCopy()). requestAuthenticator.EXPECT().AuthenticateTokenCredentialRequest(gomock.Any(), req.DeepCopy()).
Return(&user.DefaultInfo{Name: "test-user"}, nil) Return(&user.DefaultInfo{Name: "test-user"}, nil)
storage := NewREST(requestAuthenticator, successfulIssuer(ctrl)) storage := NewREST(requestAuthenticator, successfulIssuer(ctrl), schema.GroupResource{})
validationFunctionWasCalled := false validationFunctionWasCalled := false
var validationFunctionSawTokenValue string var validationFunctionSawTokenValue string
response, err := storage.Create( response, err := storage.Create(
@ -248,7 +272,7 @@ func TestCreate(t *testing.T) {
}) })
it("CreateFailsWhenRequestOptionsDryRunIsNotEmpty", func() { it("CreateFailsWhenRequestOptionsDryRunIsNotEmpty", func() {
response, err := NewREST(nil, nil).Create( response, err := NewREST(nil, nil, schema.GroupResource{}).Create(
genericapirequest.NewContext(), genericapirequest.NewContext(),
validCredentialRequest(), validCredentialRequest(),
rest.ValidateAllObjectFunc, rest.ValidateAllObjectFunc,

View File

@ -0,0 +1,96 @@
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"bytes"
"os/exec"
"testing"
"github.com/stretchr/testify/require"
"go.pinniped.dev/test/library"
)
func TestGetPinnipedCategory(t *testing.T) {
env := library.IntegrationEnv(t)
dotSuffix := "." + env.APIGroupSuffix
t.Run("category, no special params", func(t *testing.T) {
var stdOut, stdErr bytes.Buffer
cmd := exec.Command("kubectl", "get", "pinniped", "-A")
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err := cmd.Run()
require.NoError(t, err, stdErr.String(), stdOut.String())
require.Empty(t, stdErr.String())
require.NotContains(t, stdOut.String(), "MethodNotAllowed")
require.Contains(t, stdOut.String(), dotSuffix)
})
t.Run("category, table params", func(t *testing.T) {
var stdOut, stdErr bytes.Buffer
cmd := exec.Command("kubectl", "get", "pinniped", "-A", "-o", "wide", "-v", "10")
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err := cmd.Run()
require.NoError(t, err, stdErr.String(), stdOut.String())
require.NotContains(t, stdOut.String(), "MethodNotAllowed")
require.Contains(t, stdOut.String(), dotSuffix)
require.Contains(t, stdErr.String(), `"kind":"Table"`)
require.Contains(t, stdErr.String(), `"resourceVersion":"0"`)
})
t.Run("list, no special params", func(t *testing.T) {
var stdOut, stdErr bytes.Buffer
//nolint: gosec // input is part of test env
cmd := exec.Command("kubectl", "get", "tokencredentialrequests.login.concierge"+dotSuffix, "-A")
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err := cmd.Run()
require.NoError(t, err, stdErr.String(), stdOut.String())
require.Empty(t, stdOut.String())
require.NotContains(t, stdErr.String(), "MethodNotAllowed")
require.Contains(t, stdErr.String(), `No resources found`)
})
t.Run("list, table params", func(t *testing.T) {
var stdOut, stdErr bytes.Buffer
//nolint: gosec // input is part of test env
cmd := exec.Command("kubectl", "get", "tokencredentialrequests.login.concierge"+dotSuffix, "-A", "-o", "wide", "-v", "10")
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err := cmd.Run()
require.NoError(t, err, stdErr.String(), stdOut.String())
require.Empty(t, stdOut.String())
require.NotContains(t, stdErr.String(), "MethodNotAllowed")
require.Contains(t, stdErr.String(), `"kind":"Table"`)
require.Contains(t, stdErr.String(), `"resourceVersion":"0"`)
})
t.Run("raw request to see body", func(t *testing.T) {
var stdOut, stdErr bytes.Buffer
//nolint: gosec // input is part of test env
cmd := exec.Command("kubectl", "get", "--raw", "/apis/login.concierge"+dotSuffix+"/v1alpha1/tokencredentialrequests")
cmd.Stdout = &stdOut
cmd.Stderr = &stdErr
err := cmd.Run()
require.NoError(t, err, stdErr.String(), stdOut.String())
require.Empty(t, stdErr.String())
require.NotContains(t, stdOut.String(), "MethodNotAllowed")
require.Contains(t, stdOut.String(), `{"kind":"TokenCredentialRequestList","apiVersion":"login.concierge`+
dotSuffix+`/v1alpha1","metadata":{"resourceVersion":"0"},"items":[]}`)
})
}

View File

@ -72,7 +72,7 @@ func TestGetAPIResourceList(t *testing.T) {
{ {
Name: "tokencredentialrequests", Name: "tokencredentialrequests",
Kind: "TokenCredentialRequest", Kind: "TokenCredentialRequest",
Verbs: []string{"create"}, Verbs: []string{"create", "list"},
Namespaced: true, Namespaced: true,
Categories: []string{"pinniped"}, Categories: []string{"pinniped"},
}, },