mirror of
https://github.com/rancher/steve.git
synced 2025-09-01 23:47:50 +00:00
Partial extension API server store + control over printed columns (#432)
* Checkpoint * Add support for custom columns * Remove old Store and Delegate abstraction * Fix nits and rewording * Remove unused mock file * Update documentation for extension api server * Remove the need for scheme for ConvertListOptions * Rename store to utils * fixup! Remove the need for scheme for ConvertListOptions * Move watch helper to tests * Add convertError at a few places * Ignore misspell on creater * Fix comments and remove unused params * Add convertError to missing error returns * Fix watcher implementation * Document request.UserFrom and request.NamespaceFrom
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
|
||||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
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"
|
||||||
@@ -72,19 +71,19 @@ type ExtensionAPIServerOptions struct {
|
|||||||
//
|
//
|
||||||
// Use [NewExtensionAPIServer] to create an ExtensionAPIServer.
|
// Use [NewExtensionAPIServer] to create an ExtensionAPIServer.
|
||||||
//
|
//
|
||||||
// Use [InstallStore] to add a new resource store onto an existing ExtensionAPIServer.
|
// Use [ExtensionAPIServer.Install] to add a new resource store onto an existing ExtensionAPIServer.
|
||||||
// Each resources will then be reachable via /apis/<group>/<version>/<resource> as
|
// Each resources will then be reachable via /apis/<group>/<version>/<resource> as
|
||||||
// defined by the Kubernetes API.
|
// defined by the Kubernetes API.
|
||||||
//
|
//
|
||||||
// When Run() is called, a separate HTTPS server is started. This server is meant
|
// When [ExtensionAPIServer.Run] is called, a separate HTTPS server is started. This server is meant
|
||||||
// for the main kube-apiserver to communicate with our extension API server. We
|
// for the main kube-apiserver to communicate with our extension API server. We
|
||||||
// can expect the following requests from the main kube-apiserver:
|
// can expect the following requests from the main kube-apiserver:
|
||||||
//
|
//
|
||||||
// <path> <user> <groups>
|
// <path> <user> <groups>
|
||||||
// /openapi/v2 system:aggregator [system:authenticated]
|
// /openapi/v2 system:aggregator [system:authenticated]
|
||||||
// /openapi/v3 system:aggregator [system:authenticated]
|
// /openapi/v3 system:aggregator [system:authenticated]
|
||||||
// /apis system:kube-aggregator [system:masters system:authenticated]
|
// /apis system:kube-aggregator [system:masters system:authenticated]
|
||||||
// /apis/ext.cattle.io/v1 system:kube-aggregator [system:masters system:authenticated]
|
// /apis/ext.cattle.io/v1 system:kube-aggregator [system:masters system:authenticated]
|
||||||
type ExtensionAPIServer struct {
|
type ExtensionAPIServer struct {
|
||||||
codecs serializer.CodecFactory
|
codecs serializer.CodecFactory
|
||||||
scheme *runtime.Scheme
|
scheme *runtime.Scheme
|
||||||
@@ -201,34 +200,49 @@ func (s *ExtensionAPIServer) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
|||||||
s.handler.ServeHTTP(w, req)
|
s.handler.ServeHTTP(w, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
// InstallStore installs a store on the given ExtensionAPIServer object.
|
// GetAuthorizer returns the authorizer used by the extension server to authorize
|
||||||
|
// requests
|
||||||
//
|
//
|
||||||
// t and TList must be non-nil.
|
// This can be used to inject the authorizer in stores that need them.
|
||||||
//
|
func (s *ExtensionAPIServer) GetAuthorizer() authorizer.Authorizer {
|
||||||
// Here's an example store for a Token and TokenList resource in the ext.cattle.io/v1 apiVersion:
|
return s.authorizer
|
||||||
//
|
}
|
||||||
// gvk := schema.GroupVersionKind{
|
|
||||||
// Group: "ext.cattle.io",
|
|
||||||
// Version: "v1",
|
|
||||||
// Kind: "Token",
|
|
||||||
// }
|
|
||||||
// InstallStore(s, &Token{}, &TokenList{}, "tokens", "token", gvk, store)
|
|
||||||
//
|
|
||||||
// Note: Not using a method on ExtensionAPIServer object due to Go generic limitations.
|
|
||||||
func InstallStore[T runtime.Object, TList runtime.Object](
|
|
||||||
s *ExtensionAPIServer,
|
|
||||||
t T,
|
|
||||||
tList TList,
|
|
||||||
resourceName string,
|
|
||||||
singularName string,
|
|
||||||
gvk schema.GroupVersionKind,
|
|
||||||
store Store[T, TList],
|
|
||||||
) error {
|
|
||||||
|
|
||||||
if !meta.IsListType(tList) {
|
|
||||||
return fmt.Errorf("tList (%T) is not a list type", tList)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Install adds a new store to the extension API server.
|
||||||
|
//
|
||||||
|
// A store implements handlers for the various operations (verbs) supported for
|
||||||
|
// a defined GVK / GVR. For example, a store for a (apiVersion:
|
||||||
|
// ext.cattle.io/v1, kind: Tokens) Custom Resource could implement create and
|
||||||
|
// watch verbs.
|
||||||
|
//
|
||||||
|
// A store MUST implement the following interfaces: [rest.Storage], [rest.Scoper], [rest.GroupVersionKindProvider]
|
||||||
|
// and [rest.SingularNameProvider].
|
||||||
|
//
|
||||||
|
// Implementing the various verbs goes as follows:
|
||||||
|
// - get: [rest.Getter] must be implemented
|
||||||
|
// - list: [rest.Lister] must be implemented. To help implement table conversion, we provide [ConvertToTable] and [ConvertToTableDefault].
|
||||||
|
// Use [ConvertListOptions] to convert the [metainternalversion.ListOptions] to a [metav1.ListOptions].
|
||||||
|
// - watch: [rest.Watcher] must be implemented. Use [ConvertListOptions] to convert the [metainternalversion.ListOptions] to a [metav1.ListOptions].
|
||||||
|
// - create: [rest.Creater] must be implemented
|
||||||
|
// - update: [rest.Updater] must be implemented. To help implement this correctly with create-on-update support, we provide [CreateOrUpdate].
|
||||||
|
// - patch: [rest.Patcher] must be implemented, which is essentially [rest.Getter] and [rest.Updater]
|
||||||
|
// - delete: [rest.GracefulDeleter] must be implemented
|
||||||
|
// - deletecollection: [rest.CollectionDeleter] must be implemented
|
||||||
|
//
|
||||||
|
// Most of these methods have a [context.Context] parameter that can be used to get more information
|
||||||
|
// about the request. Here are some examples:
|
||||||
|
// - [request.UserFrom] to get the user info
|
||||||
|
// - [request.NamespaceFrom] to get the namespace (if applicable)
|
||||||
|
//
|
||||||
|
// For an example store implementing these, please look at the testStore type with the caveat that it is a dummy test-special purpose
|
||||||
|
// store.
|
||||||
|
//
|
||||||
|
// Note that errors returned by any operations above MUST be of type [k8s.io/apimachinery/pkg/api/errors.APIStatus].
|
||||||
|
// These can be created with [k8s.io/apimachinery/pkg/api/errors.NewNotFound], etc.
|
||||||
|
// If an error of unknown type is returned, the library will log an error message.
|
||||||
|
//
|
||||||
|
//nolint:misspell
|
||||||
|
func (s *ExtensionAPIServer) Install(resourceName string, gvk schema.GroupVersionKind, storage rest.Storage) error {
|
||||||
apiGroup, ok := s.apiGroups[gvk.Group]
|
apiGroup, ok := s.apiGroups[gvk.Group]
|
||||||
if !ok {
|
if !ok {
|
||||||
apiGroup = genericapiserver.NewDefaultAPIGroupInfo(gvk.Group, s.scheme, metav1.ParameterCodec, s.codecs)
|
apiGroup = genericapiserver.NewDefaultAPIGroupInfo(gvk.Group, s.scheme, metav1.ParameterCodec, s.codecs)
|
||||||
@@ -239,25 +253,7 @@ func InstallStore[T runtime.Object, TList runtime.Object](
|
|||||||
apiGroup.VersionedResourcesStorageMap[gvk.Version] = make(map[string]rest.Storage)
|
apiGroup.VersionedResourcesStorageMap[gvk.Version] = make(map[string]rest.Storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
del := &delegateError[T, TList]{
|
apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = storage
|
||||||
inner: &delegate[T, TList]{
|
|
||||||
scheme: s.scheme,
|
|
||||||
|
|
||||||
t: t,
|
|
||||||
tList: tList,
|
|
||||||
singularName: singularName,
|
|
||||||
gvk: gvk,
|
|
||||||
gvr: schema.GroupVersionResource{
|
|
||||||
Group: gvk.Group,
|
|
||||||
Version: gvk.Version,
|
|
||||||
Resource: resourceName,
|
|
||||||
},
|
|
||||||
authorizer: s.authorizer,
|
|
||||||
store: store,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = del
|
|
||||||
s.apiGroups[gvk.Group] = apiGroup
|
s.apiGroups[gvk.Group] = apiGroup
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package ext
|
package ext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -12,22 +13,32 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
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/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/apiserver/pkg/server/options"
|
"k8s.io/apiserver/pkg/server/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var _ rest.Storage = (*authnTestStore)(nil)
|
||||||
|
var _ rest.Lister = (*authnTestStore)(nil)
|
||||||
|
|
||||||
type authnTestStore struct {
|
type authnTestStore struct {
|
||||||
*testStore
|
*testStore[*TestType, *TestTypeList]
|
||||||
userCh chan user.Info
|
userCh chan user.Info
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *authnTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
func (t *authnTestStore) List(ctx context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||||
t.userCh <- ctx.User
|
userInfo, ok := request.UserFrom(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, convertError(fmt.Errorf("missing user info"))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.userCh <- userInfo
|
||||||
return &testTypeListFixture, nil
|
return &testTypeListFixture, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +61,10 @@ func TestAuthenticationCustom(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
store := &authnTestStore{
|
store := &authnTestStore{
|
||||||
testStore: &testStore{},
|
testStore: newDefaultTestStore(),
|
||||||
userCh: make(chan user.Info, 100),
|
userCh: make(chan user.Info, 100),
|
||||||
}
|
}
|
||||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, store, func(opts *ExtensionAPIServerOptions) {
|
||||||
opts.Listener = ln
|
opts.Listener = ln
|
||||||
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||||
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||||
|
@@ -23,32 +23,40 @@ import (
|
|||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
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/serializer"
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
||||||
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
"k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/apiserver/pkg/server/options"
|
"k8s.io/apiserver/pkg/server/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
type authzTestStore struct {
|
type authzTestStore struct {
|
||||||
*testStore
|
*testStore[*TestType, *TestTypeList]
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *authzTestStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
|
// Get implements [rest.Getter]
|
||||||
if name == "not-found" {
|
func (t *authzTestStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||||
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
return t.get(ctx, name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List implements [rest.Lister]
|
||||||
|
func (t *authzTestStore) List(ctx context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||||
|
userInfo, ok := request.UserFrom(ctx)
|
||||||
|
if !ok {
|
||||||
|
return nil, convertError(fmt.Errorf("missing user info"))
|
||||||
}
|
}
|
||||||
return t.testStore.Get(ctx, name, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
if userInfo.GetName() == "read-only-error" {
|
||||||
if ctx.User.GetName() == "read-only-error" {
|
decision, _, err := t.authorizer.Authorize(ctx, authorizer.AttributesRecord{
|
||||||
decision, _, err := ctx.Authorizer.Authorize(ctx, authorizer.AttributesRecord{
|
User: userInfo,
|
||||||
User: ctx.User,
|
|
||||||
Verb: "customverb",
|
Verb: "customverb",
|
||||||
Resource: "testtypes",
|
Resource: "testtypes",
|
||||||
ResourceRequest: true,
|
ResourceRequest: true,
|
||||||
@@ -58,7 +66,7 @@ func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeL
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
err = fmt.Errorf("not allowed")
|
err = fmt.Errorf("not allowed")
|
||||||
}
|
}
|
||||||
forbidden := apierrors.NewForbidden(ctx.GroupVersionResource.GroupResource(), "Forbidden", err)
|
forbidden := apierrors.NewForbidden(t.gvr.GroupResource(), "Forbidden", err)
|
||||||
forbidden.ErrStatus.Kind = "Status"
|
forbidden.ErrStatus.Kind = "Status"
|
||||||
forbidden.ErrStatus.APIVersion = "v1"
|
forbidden.ErrStatus.APIVersion = "v1"
|
||||||
return nil, forbidden
|
return nil, forbidden
|
||||||
@@ -67,6 +75,54 @@ func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeL
|
|||||||
return &testTypeListFixture, nil
|
return &testTypeListFixture, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *authzTestStore) get(_ context.Context, name string, _ *metav1.GetOptions) (*TestType, error) {
|
||||||
|
if name == "not-found" {
|
||||||
|
return nil, apierrors.NewNotFound(t.gvr.GroupResource(), name)
|
||||||
|
}
|
||||||
|
return &testTypeFixture, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *authzTestStore) create(_ context.Context, _ *TestType, _ *metav1.CreateOptions) (*TestType, error) {
|
||||||
|
return &testTypeFixture, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *authzTestStore) update(_ context.Context, _ *TestType, _ *metav1.UpdateOptions) (*TestType, error) {
|
||||||
|
return &testTypeFixture, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements [rest.Creater]
|
||||||
|
func (t *authzTestStore) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||||
|
if createValidation != nil {
|
||||||
|
err := createValidation(ctx, obj)
|
||||||
|
if err != nil {
|
||||||
|
return obj, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objT, ok := obj.(*TestType)
|
||||||
|
if !ok {
|
||||||
|
var zeroT *TestType
|
||||||
|
return nil, convertError(fmt.Errorf("expected %T but got %T", zeroT, obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.create(ctx, objT, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements [rest.Updater]
|
||||||
|
func (t *authzTestStore) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
|
return CreateOrUpdate(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options, t.get, t.create, t.update)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch implements [rest.Watcher]
|
||||||
|
func (t *authzTestStore) Watch(_ context.Context, _ *metainternalversion.ListOptions) (watch.Interface, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements [rest.GracefulDeleter]
|
||||||
|
func (t *authzTestStore) Delete(_ context.Context, _ string, _ rest.ValidateObjectFunc, _ *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
||||||
t := s.T()
|
t := s.T()
|
||||||
|
|
||||||
@@ -89,10 +145,7 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
|||||||
ln, _, err := options.CreateListener("", ":0", net.ListenConfig{})
|
ln, _, err := options.CreateListener("", ":0", net.ListenConfig{})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
store := &authzTestStore{
|
extensionAPIServer, cleanup, err := setupExtensionAPIServerNoStore(t, scheme, func(opts *ExtensionAPIServerOptions) {
|
||||||
testStore: &testStore{},
|
|
||||||
}
|
|
||||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
|
||||||
opts.Listener = ln
|
opts.Listener = ln
|
||||||
opts.Authorizer = authz
|
opts.Authorizer = authz
|
||||||
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||||
@@ -104,7 +157,17 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
|||||||
User: user,
|
User: user,
|
||||||
}, true, nil
|
}, true, nil
|
||||||
})
|
})
|
||||||
}, nil)
|
}, func(s *ExtensionAPIServer) error {
|
||||||
|
store := &authzTestStore{
|
||||||
|
testStore: newDefaultTestStore(),
|
||||||
|
authorizer: s.GetAuthorizer(),
|
||||||
|
}
|
||||||
|
err := s.Install("testtypes", testTypeGV.WithKind("TestType"), store)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
|
@@ -11,11 +11,11 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
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/apimachinery/pkg/runtime/schema"
|
||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
regrest "k8s.io/apiserver/pkg/registry/rest"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/rest"
|
"k8s.io/client-go/rest"
|
||||||
)
|
)
|
||||||
@@ -41,83 +42,6 @@ func authzAllowAll(ctx context.Context, a authorizer.Attributes) (authorizer.Dec
|
|||||||
return authorizer.DecisionAllow, "", nil
|
return authorizer.DecisionAllow, "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mapStore struct {
|
|
||||||
items map[string]*TestType
|
|
||||||
events chan WatchEvent[*TestType]
|
|
||||||
}
|
|
||||||
|
|
||||||
func newMapStore() *mapStore {
|
|
||||||
return &mapStore{
|
|
||||||
items: make(map[string]*TestType),
|
|
||||||
events: make(chan WatchEvent[*TestType], 100),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mapStore) Create(ctx Context, obj *TestType, opts *metav1.CreateOptions) (*TestType, error) {
|
|
||||||
if _, found := t.items[obj.Name]; found {
|
|
||||||
return nil, apierrors.NewAlreadyExists(ctx.GroupVersionResource.GroupResource(), obj.Name)
|
|
||||||
}
|
|
||||||
t.items[obj.Name] = obj
|
|
||||||
t.events <- WatchEvent[*TestType]{
|
|
||||||
Event: watch.Added,
|
|
||||||
Object: obj,
|
|
||||||
}
|
|
||||||
return obj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mapStore) Update(ctx Context, obj *TestType, opts *metav1.UpdateOptions) (*TestType, error) {
|
|
||||||
if _, found := t.items[obj.Name]; !found {
|
|
||||||
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), obj.Name)
|
|
||||||
}
|
|
||||||
obj.ManagedFields = []metav1.ManagedFieldsEntry{}
|
|
||||||
t.items[obj.Name] = obj
|
|
||||||
t.events <- WatchEvent[*TestType]{
|
|
||||||
Event: watch.Modified,
|
|
||||||
Object: obj,
|
|
||||||
}
|
|
||||||
return obj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mapStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
|
|
||||||
obj, found := t.items[name]
|
|
||||||
if !found {
|
|
||||||
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
|
||||||
}
|
|
||||||
return obj, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mapStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
|
||||||
items := []TestType{}
|
|
||||||
for _, obj := range t.items {
|
|
||||||
items = append(items, *obj)
|
|
||||||
}
|
|
||||||
sort.Slice(items, func(i, j int) bool {
|
|
||||||
return items[i].Name > items[j].Name
|
|
||||||
})
|
|
||||||
list := &TestTypeList{
|
|
||||||
Items: items,
|
|
||||||
}
|
|
||||||
return list, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mapStore) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestType], error) {
|
|
||||||
return t.events, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *mapStore) Delete(ctx Context, name string, opts *metav1.DeleteOptions) error {
|
|
||||||
obj, found := t.items[name]
|
|
||||||
if !found {
|
|
||||||
return apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(t.items, name)
|
|
||||||
t.events <- WatchEvent[*TestType]{
|
|
||||||
Event: watch.Deleted,
|
|
||||||
Object: obj,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
scheme := runtime.NewScheme()
|
scheme := runtime.NewScheme()
|
||||||
AddToScheme(scheme)
|
AddToScheme(scheme)
|
||||||
@@ -128,8 +52,10 @@ func TestStore(t *testing.T) {
|
|||||||
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0")
|
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
store := newMapStore()
|
store := newDefaultTestStore()
|
||||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
store.items = make(map[string]*TestType)
|
||||||
|
|
||||||
|
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, store, func(opts *ExtensionAPIServerOptions) {
|
||||||
opts.Listener = ln
|
opts.Listener = ln
|
||||||
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||||
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
||||||
@@ -298,36 +224,48 @@ func TestStore(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Store[*TestTypeOther, *TestTypeOtherList] = (*testStoreOther)(nil)
|
// This store tests when there's only a subset of verbs supported
|
||||||
|
type partialStorage struct {
|
||||||
// This store is meant to be able to test many stores
|
gvk schema.GroupVersionKind
|
||||||
type testStoreOther struct {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStoreOther) Create(ctx Context, obj *TestTypeOther, opts *metav1.CreateOptions) (*TestTypeOther, error) {
|
// New implements [regrest.Storage]
|
||||||
return &testTypeOtherFixture, nil
|
func (t *partialStorage) New() runtime.Object {
|
||||||
|
obj := &TestType{}
|
||||||
|
obj.GetObjectKind().SetGroupVersionKind(t.gvk)
|
||||||
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStoreOther) Update(ctx Context, obj *TestTypeOther, opts *metav1.UpdateOptions) (*TestTypeOther, error) {
|
// Destroy implements [regrest.Storage]
|
||||||
return &testTypeOtherFixture, nil
|
func (t *partialStorage) Destroy() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStoreOther) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestTypeOther, error) {
|
// GetSingularName implements [regrest.SingularNameProvider]
|
||||||
return &testTypeOtherFixture, nil
|
func (t *partialStorage) GetSingularName() string {
|
||||||
|
return "testtype"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStoreOther) List(ctx Context, opts *metav1.ListOptions) (*TestTypeOtherList, error) {
|
// NamespaceScoped implements [regrest.Scoper]
|
||||||
return &testTypeOtherListFixture, nil
|
func (t *partialStorage) NamespaceScoped() bool {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStoreOther) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestTypeOther], error) {
|
// GroupVersionKind implements [regrest.GroupVersionKindProvider]
|
||||||
|
func (t *partialStorage) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
|
||||||
|
return t.gvk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *partialStorage) Create(ctx context.Context, obj runtime.Object, createValidation regrest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||||
|
if createValidation != nil {
|
||||||
|
err := createValidation(ctx, obj)
|
||||||
|
if err != nil {
|
||||||
|
return obj, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStoreOther) Delete(ctx Context, name string, opts *metav1.DeleteOptions) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The POC had a bug where multiple resources couldn't be installed so we're
|
// The POC had a bug where multiple resources couldn't be installed so we're
|
||||||
// testing this here
|
// testing this here
|
||||||
func TestDiscoveryAndOpenAPI(t *testing.T) {
|
func TestDiscoveryAndOpenAPI(t *testing.T) {
|
||||||
@@ -343,35 +281,67 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
|||||||
Group: "ext2.cattle.io",
|
Group: "ext2.cattle.io",
|
||||||
Version: "v3",
|
Version: "v3",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partialGroupVersion := schema.GroupVersion{
|
||||||
|
Group: "ext.cattle.io",
|
||||||
|
Version: "v4",
|
||||||
|
}
|
||||||
scheme.AddKnownTypes(differentVersion, &TestType{}, &TestTypeList{})
|
scheme.AddKnownTypes(differentVersion, &TestType{}, &TestTypeList{})
|
||||||
scheme.AddKnownTypes(differentGroupVersion, &TestType{}, &TestTypeList{})
|
scheme.AddKnownTypes(differentGroupVersion, &TestType{}, &TestTypeList{})
|
||||||
|
scheme.AddKnownTypes(partialGroupVersion, &TestType{}, &TestTypeList{})
|
||||||
metav1.AddToGroupVersion(scheme, differentVersion)
|
metav1.AddToGroupVersion(scheme, differentVersion)
|
||||||
metav1.AddToGroupVersion(scheme, differentGroupVersion)
|
metav1.AddToGroupVersion(scheme, differentGroupVersion)
|
||||||
|
metav1.AddToGroupVersion(scheme, partialGroupVersion)
|
||||||
|
|
||||||
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", ":0")
|
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", ":0")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
store := &testStore{}
|
store := newDefaultTestStore()
|
||||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, store, func(opts *ExtensionAPIServerOptions) {
|
||||||
opts.Listener = ln
|
opts.Listener = ln
|
||||||
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||||
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
||||||
}, func(s *ExtensionAPIServer) error {
|
}, func(s *ExtensionAPIServer) error {
|
||||||
store := &testStoreOther{}
|
err = s.Install("testtypeothers", testTypeGV.WithKind("TestTypeOther"), &testStore[*TestTypeOther, *TestTypeOtherList]{
|
||||||
err := InstallStore(s, &TestTypeOther{}, &TestTypeOtherList{}, "testtypeothers", "testtypeother", testTypeGV.WithKind("TestTypeOther"), store)
|
singular: "testtypeother",
|
||||||
|
objT: &TestTypeOther{},
|
||||||
|
objListT: &TestTypeOtherList{},
|
||||||
|
gvk: testTypeGV.WithKind("TestTypeOther"),
|
||||||
|
gvr: schema.GroupVersionResource{Group: testTypeGV.Group, Version: testTypeGV.Version, Resource: "testtypes"},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = InstallStore(s, &TestType{}, &TestTypeList{}, "testtypes", "testtype", differentVersion.WithKind("TestType"), &testStore{})
|
err = s.Install("testtypes", differentVersion.WithKind("TestType"), &testStore[*TestType, *TestTypeList]{
|
||||||
|
singular: "testtype",
|
||||||
|
objT: &TestType{},
|
||||||
|
objListT: &TestTypeList{},
|
||||||
|
gvk: differentVersion.WithKind("TestType"),
|
||||||
|
gvr: schema.GroupVersionResource{Group: differentVersion.Group, Version: differentVersion.Version, Resource: "testtypes"},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = InstallStore(s, &TestType{}, &TestTypeList{}, "testtypes", "testtype", differentGroupVersion.WithKind("TestType"), &testStore{})
|
err = s.Install("testtypes", differentGroupVersion.WithKind("TestType"), &testStore[*TestType, *TestTypeList]{
|
||||||
|
singular: "testtype",
|
||||||
|
objT: &TestType{},
|
||||||
|
objListT: &TestTypeList{},
|
||||||
|
gvk: differentGroupVersion.WithKind("TestType"),
|
||||||
|
gvr: schema.GroupVersionResource{Group: differentGroupVersion.Group, Version: differentVersion.Version, Resource: "testtypes"},
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = s.Install("testtypes", partialGroupVersion.WithKind("TestType"), &partialStorage{
|
||||||
|
gvk: partialGroupVersion.WithKind("TestType"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -401,6 +371,10 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
|||||||
{
|
{
|
||||||
Name: "ext.cattle.io",
|
Name: "ext.cattle.io",
|
||||||
Versions: []metav1.GroupVersionForDiscovery{
|
Versions: []metav1.GroupVersionForDiscovery{
|
||||||
|
{
|
||||||
|
GroupVersion: "ext.cattle.io/v4",
|
||||||
|
Version: "v4",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
GroupVersion: "ext.cattle.io/v2",
|
GroupVersion: "ext.cattle.io/v2",
|
||||||
Version: "v2",
|
Version: "v2",
|
||||||
@@ -450,6 +424,10 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
|||||||
GroupVersion: "ext.cattle.io/v2",
|
GroupVersion: "ext.cattle.io/v2",
|
||||||
Version: "v2",
|
Version: "v2",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
GroupVersion: "ext.cattle.io/v4",
|
||||||
|
Version: "v4",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
GroupVersion: "ext.cattle.io/v1",
|
GroupVersion: "ext.cattle.io/v1",
|
||||||
Version: "v1",
|
Version: "v1",
|
||||||
@@ -569,6 +547,32 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/apis/ext.cattle.io/v4",
|
||||||
|
got: &metav1.APIResourceList{},
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: &metav1.APIResourceList{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "APIResourceList",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
GroupVersion: "ext.cattle.io/v4",
|
||||||
|
APIResources: []metav1.APIResource{
|
||||||
|
{
|
||||||
|
Name: "testtypes",
|
||||||
|
SingularName: "testtype",
|
||||||
|
Namespaced: false,
|
||||||
|
Kind: "TestType",
|
||||||
|
Group: "ext.cattle.io",
|
||||||
|
Version: "v4",
|
||||||
|
// Only the create verb is supported for this store
|
||||||
|
Verbs: metav1.Verbs{
|
||||||
|
"create",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/openapi/v2",
|
path: "/openapi/v2",
|
||||||
expectedStatusCode: http.StatusOK,
|
expectedStatusCode: http.StatusOK,
|
||||||
@@ -664,15 +668,29 @@ func TestNoStore(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupExtensionAPIServer[
|
func setupExtensionAPIServer(
|
||||||
T runtime.Object,
|
t *testing.T,
|
||||||
TList runtime.Object,
|
scheme *runtime.Scheme,
|
||||||
](
|
store regrest.Storage,
|
||||||
|
optionSetter func(*ExtensionAPIServerOptions),
|
||||||
|
extensionAPIServerSetter func(*ExtensionAPIServer) error,
|
||||||
|
) (*ExtensionAPIServer, func(), error) {
|
||||||
|
fn := func(e *ExtensionAPIServer) error {
|
||||||
|
err := e.Install("testtypes", testTypeGV.WithKind("TestType"), store)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("InstallStore: %w", err)
|
||||||
|
}
|
||||||
|
if extensionAPIServerSetter != nil {
|
||||||
|
return extensionAPIServerSetter(e)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return setupExtensionAPIServerNoStore(t, scheme, optionSetter, fn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupExtensionAPIServerNoStore(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
scheme *runtime.Scheme,
|
scheme *runtime.Scheme,
|
||||||
objT T,
|
|
||||||
objTList TList,
|
|
||||||
store Store[T, TList],
|
|
||||||
optionSetter func(*ExtensionAPIServerOptions),
|
optionSetter func(*ExtensionAPIServerOptions),
|
||||||
extensionAPIServerSetter func(*ExtensionAPIServer) error,
|
extensionAPIServerSetter func(*ExtensionAPIServer) error,
|
||||||
) (*ExtensionAPIServer, func(), error) {
|
) (*ExtensionAPIServer, func(), error) {
|
||||||
@@ -694,11 +712,6 @@ func setupExtensionAPIServer[
|
|||||||
return nil, func() {}, err
|
return nil, func() {}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = InstallStore(extensionAPIServer, objT, objTList, "testtypes", "testtype", testTypeGV.WithKind("TestType"), store)
|
|
||||||
if err != nil {
|
|
||||||
return nil, func() {}, fmt.Errorf("InstallStore: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if extensionAPIServerSetter != nil {
|
if extensionAPIServerSetter != nil {
|
||||||
err = extensionAPIServerSetter(extensionAPIServer)
|
err = extensionAPIServerSetter(extensionAPIServer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -768,3 +781,222 @@ func createRecordingWatcher(scheme *runtime.Scheme, gvr schema.GroupVersionResou
|
|||||||
stop: myWatch.Stop,
|
stop: myWatch.Stop,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This store tests the printed columns functionality
|
||||||
|
type customColumnsStore struct {
|
||||||
|
*testStore[*TestType, *TestTypeList]
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
columns []metav1.TableColumnDefinition
|
||||||
|
convertFn func(obj *TestType) []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customColumnsStore) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
return ConvertToTable(ctx, object, tableOptions, s.testStore.gvr.GroupResource(), s.columns, s.convertFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *customColumnsStore) Set(columns []metav1.TableColumnDefinition, convertFn func(obj *TestType) []string) {
|
||||||
|
s.lock.Lock()
|
||||||
|
defer s.lock.Unlock()
|
||||||
|
s.columns = columns
|
||||||
|
s.convertFn = convertFn
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCustomColumns(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
AddToScheme(scheme)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
store := &customColumnsStore{
|
||||||
|
testStore: newDefaultTestStore(),
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionAPIServer, cleanup, err := setupExtensionAPIServerNoStore(t, scheme, func(opts *ExtensionAPIServerOptions) {
|
||||||
|
opts.Listener = ln
|
||||||
|
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||||
|
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
||||||
|
}, func(s *ExtensionAPIServer) error {
|
||||||
|
err := s.Install("testtypes", testTypeGV.WithKind("TestType"), store)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
ts := httptest.NewServer(extensionAPIServer)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
createRequest := func(path string) *http.Request {
|
||||||
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||||||
|
// This asks the apiserver to give back a metav1.Table for List and Get operations
|
||||||
|
req.Header.Add("Accept", "application/json;as=Table;v=v1;g=meta.k8s.io")
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
columns := []metav1.TableColumnDefinition{
|
||||||
|
{
|
||||||
|
Name: "Name",
|
||||||
|
Type: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Foo",
|
||||||
|
Type: "string",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "Bar",
|
||||||
|
Type: "number",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
convertFn := func(obj *TestType) []string {
|
||||||
|
return []string{
|
||||||
|
"the name is " + obj.GetName(),
|
||||||
|
"the foo value",
|
||||||
|
"the bar value",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
requests []*http.Request
|
||||||
|
columns []metav1.TableColumnDefinition
|
||||||
|
convertFn func(obj *TestType) []string
|
||||||
|
expectedStatusCode int
|
||||||
|
expectedBody any
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
requests: []*http.Request{
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes"),
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes/foo"),
|
||||||
|
},
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: &metav1.Table{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1"},
|
||||||
|
ColumnDefinitions: []metav1.TableColumnDefinition{
|
||||||
|
{Name: "Name", Type: "string", Format: "name", Description: "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names"},
|
||||||
|
{Name: "Created At", Type: "date", Description: "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata"},
|
||||||
|
},
|
||||||
|
Rows: []metav1.TableRow{
|
||||||
|
{
|
||||||
|
Cells: []any{"foo", "0001-01-01T00:00:00Z"},
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind":"PartialObjectMetadata","apiVersion":"meta.k8s.io/v1","metadata":{"name":"foo","creationTimestamp":null}}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom include object default and metadata",
|
||||||
|
requests: []*http.Request{
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes"),
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes/foo"),
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes?includeObject=Metadata"),
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes/foo?includeObject=Metadata"),
|
||||||
|
},
|
||||||
|
columns: columns,
|
||||||
|
convertFn: convertFn,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: &metav1.Table{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1"},
|
||||||
|
ColumnDefinitions: []metav1.TableColumnDefinition{
|
||||||
|
{Name: "Name", Type: "name"},
|
||||||
|
{Name: "Foo", Type: "string"},
|
||||||
|
{Name: "Bar", Type: "number"},
|
||||||
|
},
|
||||||
|
Rows: []metav1.TableRow{
|
||||||
|
{
|
||||||
|
Cells: []any{"the name is foo", "the foo value", "the bar value"},
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind":"PartialObjectMetadata","apiVersion":"meta.k8s.io/v1","metadata":{"name":"foo","creationTimestamp":null}}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom include object None",
|
||||||
|
requests: []*http.Request{
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes?includeObject=None"),
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes/foo?includeObject=None"),
|
||||||
|
},
|
||||||
|
columns: columns,
|
||||||
|
convertFn: convertFn,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: &metav1.Table{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1"},
|
||||||
|
ColumnDefinitions: []metav1.TableColumnDefinition{
|
||||||
|
{Name: "Name", Type: "name"},
|
||||||
|
{Name: "Foo", Type: "string"},
|
||||||
|
{Name: "Bar", Type: "number"},
|
||||||
|
},
|
||||||
|
Rows: []metav1.TableRow{
|
||||||
|
{
|
||||||
|
Cells: []any{"the name is foo", "the foo value", "the bar value"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "custom include object Object",
|
||||||
|
requests: []*http.Request{
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes?includeObject=Object"),
|
||||||
|
createRequest("/apis/ext.cattle.io/v1/testtypes/foo?includeObject=Object"),
|
||||||
|
},
|
||||||
|
columns: columns,
|
||||||
|
convertFn: convertFn,
|
||||||
|
expectedStatusCode: http.StatusOK,
|
||||||
|
expectedBody: &metav1.Table{
|
||||||
|
TypeMeta: metav1.TypeMeta{Kind: "Table", APIVersion: "meta.k8s.io/v1"},
|
||||||
|
ColumnDefinitions: []metav1.TableColumnDefinition{
|
||||||
|
{Name: "Name", Type: "name"},
|
||||||
|
{Name: "Foo", Type: "string"},
|
||||||
|
{Name: "Bar", Type: "number"},
|
||||||
|
},
|
||||||
|
Rows: []metav1.TableRow{
|
||||||
|
{
|
||||||
|
Cells: []any{"the name is foo", "the foo value", "the bar value"},
|
||||||
|
Object: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"kind":"TestType","apiVersion":"ext.cattle.io/v1","metadata":{"name":"foo","creationTimestamp":null}}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.columns != nil {
|
||||||
|
store.Set(test.columns, test.convertFn)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, req := range test.requests {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
extensionAPIServer.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
resp := w.Result()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
require.Equal(t, test.expectedStatusCode, resp.StatusCode)
|
||||||
|
if test.expectedBody != nil {
|
||||||
|
table := &metav1.Table{}
|
||||||
|
err = json.Unmarshal(body, table)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, test.expectedBody, table)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@@ -1,356 +0,0 @@
|
|||||||
package ext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
||||||
"k8s.io/apiserver/pkg/endpoints/request"
|
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
errMissingUserInfo = errors.New("missing user info")
|
|
||||||
)
|
|
||||||
|
|
||||||
// delegate is the bridge between k8s.io/apiserver's [rest.Storage] interface and
|
|
||||||
// our own Store interface we want developers to use
|
|
||||||
//
|
|
||||||
// It currently supports non-namespaced stores only because Store[T, TList] doesn't
|
|
||||||
// expose namespaces anywhere. When needed we'll add support to namespaced resources.
|
|
||||||
type delegate[T runtime.Object, TList runtime.Object] struct {
|
|
||||||
scheme *runtime.Scheme
|
|
||||||
// t is the resource of the delegate (eg: *Token) and must be non-nil.
|
|
||||||
t T
|
|
||||||
// tList is the resource list of the delegate (eg: *TokenList) and must be non-nil.
|
|
||||||
tList TList
|
|
||||||
gvk schema.GroupVersionKind
|
|
||||||
gvr schema.GroupVersionResource
|
|
||||||
singularName string
|
|
||||||
store Store[T, TList]
|
|
||||||
authorizer authorizer.Authorizer
|
|
||||||
}
|
|
||||||
|
|
||||||
// New implements [rest.Storage]
|
|
||||||
//
|
|
||||||
// It uses generics to create the resource and set its GVK.
|
|
||||||
func (s *delegate[T, TList]) New() runtime.Object {
|
|
||||||
t := s.t.DeepCopyObject()
|
|
||||||
t.GetObjectKind().SetGroupVersionKind(s.gvk)
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
|
|
||||||
// Destroy cleans up its resources on shutdown.
|
|
||||||
// Destroy has to be implemented in thread-safe way and be prepared
|
|
||||||
// for being called more than once.
|
|
||||||
//
|
|
||||||
// It is NOT meant to delete resources from the backing storage. It is meant to
|
|
||||||
// stop clients, runners, etc that could be running for the store when the extension
|
|
||||||
// API server gracefully shutdowns/exits.
|
|
||||||
func (s *delegate[T, TList]) Destroy() {
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewList implements [rest.Lister]
|
|
||||||
//
|
|
||||||
// It uses generics to create the resource and set its GVK.
|
|
||||||
func (s *delegate[T, TList]) NewList() runtime.Object {
|
|
||||||
tList := s.tList.DeepCopyObject()
|
|
||||||
tList.GetObjectKind().SetGroupVersionKind(s.gvk)
|
|
||||||
return tList
|
|
||||||
}
|
|
||||||
|
|
||||||
// List implements [rest.Lister]
|
|
||||||
func (s *delegate[T, TList]) List(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (runtime.Object, error) {
|
|
||||||
ctx, err := s.makeContext(parentCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
options, err := s.convertListOptions(internaloptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.store.List(ctx, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertToTable implements [rest.Lister]
|
|
||||||
//
|
|
||||||
// It converts an object or a list of objects to a table, which is used by kubectl
|
|
||||||
// (and Rancher UI) to display a table of the items.
|
|
||||||
//
|
|
||||||
// Currently, we use the default table convertor which will show two columns: Name and Created At.
|
|
||||||
func (s *delegate[T, TList]) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
|
||||||
defaultTableConverter := rest.NewDefaultTableConvertor(s.gvr.GroupResource())
|
|
||||||
return defaultTableConverter.ConvertToTable(ctx, object, tableOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get implements [rest.Getter]
|
|
||||||
func (s *delegate[T, TList]) Get(parentCtx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
|
||||||
ctx, err := s.makeContext(parentCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.store.Get(ctx, name, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete implements [rest.GracefulDeleter]
|
|
||||||
//
|
|
||||||
// deleteValidation is used to do some validation on the object before deleting
|
|
||||||
// it in the store. For example, running mutating/validating webhooks, though we're not using these yet.
|
|
||||||
func (s *delegate[T, TList]) Delete(parentCtx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
|
||||||
ctx, err := s.makeContext(parentCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldObj, err := s.store.Get(ctx, name, &metav1.GetOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if deleteValidation != nil {
|
|
||||||
if err = deleteValidation(ctx, oldObj); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.store.Delete(ctx, name, options)
|
|
||||||
return oldObj, true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create implements [rest.Creater]
|
|
||||||
//
|
|
||||||
// createValidation is used to do some validation on the object before creating
|
|
||||||
// it in the store. For example, running mutating/validating webhooks, though we're not using these yet.
|
|
||||||
//
|
|
||||||
//nolint:misspell
|
|
||||||
func (s *delegate[T, TList]) Create(parentCtx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
|
||||||
ctx, err := s.makeContext(parentCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if createValidation != nil {
|
|
||||||
err := createValidation(ctx, obj)
|
|
||||||
if err != nil {
|
|
||||||
return obj, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tObj, ok := obj.(T)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("object was of type %T, not of expected type %T", obj, s.t)
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.store.Create(ctx, tObj, options)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update implements [rest.Updater]
|
|
||||||
//
|
|
||||||
// createValidation is used to do some validation on the object before creating
|
|
||||||
// it in the store. For example, it will do an authorization check for "create"
|
|
||||||
// verb if the object needs to be created.
|
|
||||||
// See here for details: https://github.com/kubernetes/apiserver/blob/70ed6fdbea9eb37bd1d7558e90c20cfe888955e8/pkg/endpoints/handlers/update.go#L190-L201
|
|
||||||
// Another example is running mutating/validating webhooks, though we're not using these yet.
|
|
||||||
//
|
|
||||||
// updateValidation is used to do some validation on the object before updating it in the store.
|
|
||||||
// One example is running mutating/validating webhooks, though we're not using these yet.
|
|
||||||
func (s *delegate[T, TList]) Update(
|
|
||||||
parentCtx context.Context,
|
|
||||||
name string,
|
|
||||||
objInfo rest.UpdatedObjectInfo,
|
|
||||||
createValidation rest.ValidateObjectFunc,
|
|
||||||
updateValidation rest.ValidateObjectUpdateFunc,
|
|
||||||
forceAllowCreate bool,
|
|
||||||
options *metav1.UpdateOptions,
|
|
||||||
) (runtime.Object, bool, error) {
|
|
||||||
ctx, err := s.makeContext(parentCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldObj, err := s.store.Get(ctx, name, &metav1.GetOptions{})
|
|
||||||
if err != nil {
|
|
||||||
if !apierrors.IsNotFound(err) {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
obj, err := objInfo.UpdatedObject(ctx, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = createValidation(ctx, obj); err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tObj, ok := obj.(T)
|
|
||||||
if !ok {
|
|
||||||
return nil, false, fmt.Errorf("object was of type %T, not of expected type %T", obj, s.t)
|
|
||||||
}
|
|
||||||
|
|
||||||
newObj, err := s.store.Create(ctx, tObj, &metav1.CreateOptions{})
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return newObj, true, err
|
|
||||||
}
|
|
||||||
|
|
||||||
newObj, err := objInfo.UpdatedObject(ctx, oldObj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
newT, ok := newObj.(T)
|
|
||||||
if !ok {
|
|
||||||
return nil, false, fmt.Errorf("object was of type %T, not of expected type %T", newObj, s.t)
|
|
||||||
}
|
|
||||||
|
|
||||||
if updateValidation != nil {
|
|
||||||
err = updateValidation(ctx, newT, oldObj)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newT, err = s.store.Update(ctx, newT, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return newT, false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type watcher struct {
|
|
||||||
closedLock sync.RWMutex
|
|
||||||
closed bool
|
|
||||||
ch chan watch.Event
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *watcher) Stop() {
|
|
||||||
w.closedLock.Lock()
|
|
||||||
defer w.closedLock.Unlock()
|
|
||||||
if !w.closed {
|
|
||||||
close(w.ch)
|
|
||||||
w.closed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *watcher) addEvent(event watch.Event) bool {
|
|
||||||
w.closedLock.RLock()
|
|
||||||
defer w.closedLock.RUnlock()
|
|
||||||
if w.closed {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
w.ch <- event
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *watcher) ResultChan() <-chan watch.Event {
|
|
||||||
return w.ch
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *delegate[T, TList]) Watch(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (watch.Interface, error) {
|
|
||||||
ctx, err := s.makeContext(parentCtx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
options, err := s.convertListOptions(internaloptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w := &watcher{
|
|
||||||
ch: make(chan watch.Event),
|
|
||||||
}
|
|
||||||
go func() {
|
|
||||||
// Not much point continuing the watch if the store stopped its watch.
|
|
||||||
// Double stopping here is fine.
|
|
||||||
defer w.Stop()
|
|
||||||
|
|
||||||
// Closing eventCh is the responsibility of the store.Watch method
|
|
||||||
// to avoid the store panicking while trying to send to a close channel
|
|
||||||
eventCh, err := s.store.Watch(ctx, options)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
for event := range eventCh {
|
|
||||||
added := w.addEvent(watch.Event{
|
|
||||||
Type: event.Event,
|
|
||||||
Object: event.Object,
|
|
||||||
})
|
|
||||||
if !added {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupVersionKind implements rest.GroupVersionKind
|
|
||||||
//
|
|
||||||
// This is used to generate the data for the Discovery API
|
|
||||||
func (s *delegate[T, TList]) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
|
|
||||||
return s.gvk
|
|
||||||
}
|
|
||||||
|
|
||||||
// NamespaceScoped implements rest.Scoper
|
|
||||||
//
|
|
||||||
// The delegate is used for non-namespaced resources so it always returns false
|
|
||||||
func (s *delegate[T, TList]) NamespaceScoped() bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kind implements rest.KindProvider
|
|
||||||
//
|
|
||||||
// XXX: Example where / how this is used
|
|
||||||
func (s *delegate[T, TList]) Kind() string {
|
|
||||||
return s.gvk.Kind
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSingularName implements rest.SingularNameProvider
|
|
||||||
//
|
|
||||||
// This is used by a variety of things such as kubectl to map singular name to
|
|
||||||
// resource name. (eg: token => tokens)
|
|
||||||
func (s *delegate[T, TList]) GetSingularName() string {
|
|
||||||
return s.singularName
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *delegate[T, TList]) makeContext(parentCtx context.Context) (Context, error) {
|
|
||||||
userInfo, ok := request.UserFrom(parentCtx)
|
|
||||||
if !ok {
|
|
||||||
return Context{}, errMissingUserInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := Context{
|
|
||||||
Context: parentCtx,
|
|
||||||
User: userInfo,
|
|
||||||
Authorizer: s.authorizer,
|
|
||||||
GroupVersionResource: s.gvr,
|
|
||||||
}
|
|
||||||
return ctx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *delegate[T, TList]) convertListOptions(options *metainternalversion.ListOptions) (*metav1.ListOptions, error) {
|
|
||||||
var out metav1.ListOptions
|
|
||||||
err := s.scheme.Convert(options, &out, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("convert list options: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &out, nil
|
|
||||||
}
|
|
@@ -1,110 +0,0 @@
|
|||||||
package ext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
|
||||||
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// delegateError wraps an inner delegate and converts unknown errors.
|
|
||||||
type delegateError[T runtime.Object, TList runtime.Object] struct {
|
|
||||||
inner *delegate[T, TList]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) New() runtime.Object {
|
|
||||||
return d.inner.New()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Destroy() {
|
|
||||||
d.inner.Destroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) NewList() runtime.Object {
|
|
||||||
return d.inner.NewList()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) List(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (runtime.Object, error) {
|
|
||||||
result, err := d.inner.List(parentCtx, internaloptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, convertError(err)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
|
||||||
result, err := d.inner.ConvertToTable(ctx, object, tableOptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, convertError(err)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Get(parentCtx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
|
||||||
result, err := d.inner.Get(parentCtx, name, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, convertError(err)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Delete(parentCtx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
|
||||||
result, completed, err := d.inner.Delete(parentCtx, name, deleteValidation, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, convertError(err)
|
|
||||||
}
|
|
||||||
return result, completed, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Create(parentCtx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
|
||||||
result, err := d.inner.Create(parentCtx, obj, createValidation, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, convertError(err)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Update(parentCtx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
|
||||||
result, created, err := d.inner.Update(parentCtx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, convertError(err)
|
|
||||||
}
|
|
||||||
return result, created, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Watch(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (watch.Interface, error) {
|
|
||||||
result, err := d.inner.Watch(parentCtx, internaloptions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, convertError(err)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) GroupVersionKind(groupVersion schema.GroupVersion) schema.GroupVersionKind {
|
|
||||||
return d.inner.GroupVersionKind(groupVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) NamespaceScoped() bool {
|
|
||||||
return d.inner.NamespaceScoped()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) Kind() string {
|
|
||||||
return d.inner.Kind()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *delegateError[T, TList]) GetSingularName() string {
|
|
||||||
return d.inner.GetSingularName()
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertError(err error) error {
|
|
||||||
if _, ok := err.(errors.APIStatus); ok {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.NewInternalError(err)
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,17 @@
|
|||||||
package ext
|
package ext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
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/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/watch"
|
||||||
"k8s.io/apiserver/pkg/registry/rest"
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
common "k8s.io/kube-openapi/pkg/common"
|
common "k8s.io/kube-openapi/pkg/common"
|
||||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||||
@@ -162,33 +170,239 @@ func (t *TestTypeOther) DeepCopyObject() runtime.Object {
|
|||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ Store[*TestType, *TestTypeList] = (*testStore)(nil)
|
var _ rest.Storage = (*testStore[*TestType, *TestTypeList])(nil)
|
||||||
|
var _ rest.Lister = (*testStore[*TestType, *TestTypeList])(nil)
|
||||||
|
var _ rest.GracefulDeleter = (*testStore[*TestType, *TestTypeList])(nil)
|
||||||
|
var _ rest.Creater = (*testStore[*TestType, *TestTypeList])(nil)
|
||||||
|
var _ rest.Updater = (*testStore[*TestType, *TestTypeList])(nil)
|
||||||
|
var _ rest.Getter = (*testStore[*TestType, *TestTypeList])(nil)
|
||||||
|
|
||||||
type testStore struct {
|
type testStore[T runtime.Object, TList runtime.Object] struct {
|
||||||
|
singular string
|
||||||
|
objT T
|
||||||
|
objListT TList
|
||||||
|
gvk schema.GroupVersionKind
|
||||||
|
gvr schema.GroupVersionResource
|
||||||
|
|
||||||
|
// lock protects both items and watcher
|
||||||
|
lock sync.Mutex
|
||||||
|
items map[string]*TestType
|
||||||
|
watcher *watcher
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStore) Create(ctx Context, obj *TestType, opts *metav1.CreateOptions) (*TestType, error) {
|
func newDefaultTestStore() *testStore[*TestType, *TestTypeList] {
|
||||||
return &testTypeFixture, nil
|
return &testStore[*TestType, *TestTypeList]{
|
||||||
|
singular: "testtype",
|
||||||
|
objT: &TestType{},
|
||||||
|
objListT: &TestTypeList{},
|
||||||
|
gvk: testTypeGV.WithKind("TestType"),
|
||||||
|
gvr: schema.GroupVersionResource{Group: testTypeGV.Group, Version: testTypeGV.Version, Resource: "testtypes"},
|
||||||
|
items: map[string]*TestType{
|
||||||
|
testTypeFixture.Name: &testTypeFixture,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStore) Update(ctx Context, obj *TestType, opts *metav1.UpdateOptions) (*TestType, error) {
|
// New implements [rest.Storage]
|
||||||
return &testTypeFixture, nil
|
func (t *testStore[T, TList]) New() runtime.Object {
|
||||||
|
obj := t.objT.DeepCopyObject()
|
||||||
|
obj.GetObjectKind().SetGroupVersionKind(t.gvk)
|
||||||
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
|
// GetSingularName implements [rest.SingularNameProvider]
|
||||||
return &testTypeFixture, nil
|
func (t *testStore[T, TList]) GetSingularName() string {
|
||||||
|
return t.singular
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
// NamespaceScoped implements [rest.Scoper]
|
||||||
return &testTypeListFixture, nil
|
func (t *testStore[T, TList]) NamespaceScoped() bool {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStore) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestType], error) {
|
// GroupVersionKind implements [rest.GroupVersionKindProvider]
|
||||||
return nil, nil
|
func (t *testStore[T, TList]) GroupVersionKind(_ schema.GroupVersion) schema.GroupVersionKind {
|
||||||
|
return t.gvk
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *testStore) Delete(ctx Context, name string, opts *metav1.DeleteOptions) error {
|
// Destroy implements [rest.Storage]
|
||||||
return nil
|
func (t *testStore[T, TList]) Destroy() {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get implements [rest.Getter]
|
||||||
|
func (t *testStore[T, TList]) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
return t.get(ctx, name, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create implements [rest.Creater]
|
||||||
|
func (t *testStore[T, TList]) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
if createValidation != nil {
|
||||||
|
err := createValidation(ctx, obj)
|
||||||
|
if err != nil {
|
||||||
|
return obj, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
objT, ok := obj.(*TestType)
|
||||||
|
if !ok {
|
||||||
|
var zeroT T
|
||||||
|
return nil, convertError(fmt.Errorf("expected %T but got %T", zeroT, obj))
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.create(ctx, objT, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update implements [rest.Updater]
|
||||||
|
func (t *testStore[T, TList]) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
return CreateOrUpdate(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options, t.get, t.create, t.update)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStore[T, TList]) get(_ context.Context, name string, _ *metav1.GetOptions) (*TestType, error) {
|
||||||
|
obj, found := t.items[name]
|
||||||
|
if !found {
|
||||||
|
return nil, apierrors.NewNotFound(t.gvr.GroupResource(), name)
|
||||||
|
}
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStore[T, TList]) create(_ context.Context, obj *TestType, _ *metav1.CreateOptions) (*TestType, error) {
|
||||||
|
if _, found := t.items[obj.Name]; found {
|
||||||
|
return nil, apierrors.NewAlreadyExists(t.gvr.GroupResource(), obj.Name)
|
||||||
|
}
|
||||||
|
t.items[obj.Name] = obj
|
||||||
|
t.addEventLocked(watch.Event{
|
||||||
|
Type: watch.Added,
|
||||||
|
Object: obj,
|
||||||
|
})
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStore[T, TList]) update(_ context.Context, obj *TestType, _ *metav1.UpdateOptions) (*TestType, error) {
|
||||||
|
if _, found := t.items[obj.Name]; !found {
|
||||||
|
return nil, apierrors.NewNotFound(t.gvr.GroupResource(), obj.Name)
|
||||||
|
}
|
||||||
|
obj.ManagedFields = []metav1.ManagedFieldsEntry{}
|
||||||
|
t.items[obj.Name] = obj
|
||||||
|
t.addEventLocked(watch.Event{
|
||||||
|
Type: watch.Modified,
|
||||||
|
Object: obj,
|
||||||
|
})
|
||||||
|
return obj, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewList implements [rest.Lister]
|
||||||
|
func (t *testStore[T, TList]) NewList() runtime.Object {
|
||||||
|
objList := t.objListT.DeepCopyObject()
|
||||||
|
objList.GetObjectKind().SetGroupVersionKind(t.gvk)
|
||||||
|
return objList
|
||||||
|
}
|
||||||
|
|
||||||
|
// List implements [rest.Lister]
|
||||||
|
func (t *testStore[T, TList]) List(ctx context.Context, options *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
items := []TestType{}
|
||||||
|
for _, obj := range t.items {
|
||||||
|
items = append(items, *obj)
|
||||||
|
}
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].Name > items[j].Name
|
||||||
|
})
|
||||||
|
list := &TestTypeList{
|
||||||
|
Items: items,
|
||||||
|
}
|
||||||
|
return list, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToTable implements [rest.Lister]
|
||||||
|
func (t *testStore[T, TList]) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
|
||||||
|
return ConvertToTableDefault[T](ctx, object, tableOptions, t.gvr.GroupResource())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch implements [rest.Watcher]
|
||||||
|
func (t *testStore[T, TList]) Watch(ctx context.Context, internaloptions *metainternalversion.ListOptions) (watch.Interface, error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
w := &watcher{
|
||||||
|
ch: make(chan watch.Event, 100),
|
||||||
|
}
|
||||||
|
t.watcher = w
|
||||||
|
return w, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testStore[T, TList]) addEventLocked(event watch.Event) {
|
||||||
|
if t.watcher != nil {
|
||||||
|
t.watcher.addEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete implements [rest.GracefulDeleter]
|
||||||
|
func (t *testStore[T, TList]) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
|
||||||
|
t.lock.Lock()
|
||||||
|
defer t.lock.Unlock()
|
||||||
|
|
||||||
|
obj, found := t.items[name]
|
||||||
|
if !found {
|
||||||
|
return nil, false, apierrors.NewNotFound(t.gvr.GroupResource(), name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleteValidation != nil {
|
||||||
|
err := deleteValidation(ctx, obj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(t.items, name)
|
||||||
|
t.addEventLocked(watch.Event{
|
||||||
|
Type: watch.Deleted,
|
||||||
|
Object: obj,
|
||||||
|
})
|
||||||
|
return obj, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type watcher struct {
|
||||||
|
closedLock sync.RWMutex
|
||||||
|
closed bool
|
||||||
|
ch chan watch.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop implements [watch.Interface]
|
||||||
|
//
|
||||||
|
// As documented, Stop must only be called by the consumer (the k8s library) not the producer (our store)
|
||||||
|
func (w *watcher) Stop() {
|
||||||
|
w.closedLock.Lock()
|
||||||
|
defer w.closedLock.Unlock()
|
||||||
|
if !w.closed {
|
||||||
|
close(w.ch)
|
||||||
|
w.closed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResultChan implements [watch.Interface]
|
||||||
|
func (w *watcher) ResultChan() <-chan watch.Event {
|
||||||
|
return w.ch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *watcher) addEvent(event watch.Event) bool {
|
||||||
|
w.closedLock.RLock()
|
||||||
|
defer w.closedLock.RUnlock()
|
||||||
|
if w.closed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
w.ch <- event
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// This was autogenerated.
|
// This was autogenerated.
|
||||||
@@ -2929,18 +3143,3 @@ func schema_k8sio_apimachinery_pkg_version_Info(ref common.ReferenceCallback) co
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// XXX: Implement DeleteCollection to simplify everything here
|
|
||||||
// var _ rest.StandardStorage = (*delegate[*TestType, typeChecker, *typeCheckerList, typeCheckerList])(nil)
|
|
||||||
var _ rest.Storage = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Scoper = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.KindProvider = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.GroupVersionKindProvider = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.SingularNameProvider = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Getter = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Lister = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.GracefulDeleter = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Creater = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Updater = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Watcher = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
var _ rest.Patcher = (*delegate[*TestType, *TestTypeList])(nil)
|
|
||||||
|
@@ -1,66 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
|
|
||||||
// Package ext is a generated GoMock package.
|
|
||||||
package ext
|
|
||||||
|
|
||||||
import (
|
|
||||||
context "context"
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "go.uber.org/mock/gomock"
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockUpdatedObjectInfo is a mock of UpdatedObjectInfo interface.
|
|
||||||
type MockUpdatedObjectInfo struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockUpdatedObjectInfoMockRecorder
|
|
||||||
isgomock struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockUpdatedObjectInfoMockRecorder is the mock recorder for MockUpdatedObjectInfo.
|
|
||||||
type MockUpdatedObjectInfoMockRecorder struct {
|
|
||||||
mock *MockUpdatedObjectInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockUpdatedObjectInfo creates a new mock instance.
|
|
||||||
func NewMockUpdatedObjectInfo(ctrl *gomock.Controller) *MockUpdatedObjectInfo {
|
|
||||||
mock := &MockUpdatedObjectInfo{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockUpdatedObjectInfoMockRecorder{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockUpdatedObjectInfo) EXPECT() *MockUpdatedObjectInfoMockRecorder {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preconditions mocks base method.
|
|
||||||
func (m *MockUpdatedObjectInfo) Preconditions() *v1.Preconditions {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Preconditions")
|
|
||||||
ret0, _ := ret[0].(*v1.Preconditions)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Preconditions indicates an expected call of Preconditions.
|
|
||||||
func (mr *MockUpdatedObjectInfoMockRecorder) Preconditions() *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Preconditions", reflect.TypeOf((*MockUpdatedObjectInfo)(nil).Preconditions))
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatedObject mocks base method.
|
|
||||||
func (m *MockUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "UpdatedObject", ctx, oldObj)
|
|
||||||
ret0, _ := ret[0].(runtime.Object)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatedObject indicates an expected call of UpdatedObject.
|
|
||||||
func (mr *MockUpdatedObjectInfoMockRecorder) UpdatedObject(ctx, oldObj any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedObject", reflect.TypeOf((*MockUpdatedObjectInfo)(nil).UpdatedObject), ctx, oldObj)
|
|
||||||
}
|
|
@@ -1,93 +0,0 @@
|
|||||||
package ext
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Context wraps a context.Context and adds a few fields that will be useful for
|
|
||||||
// each requests handled by a Store.
|
|
||||||
//
|
|
||||||
// It will allow us to add more such fields without breaking Store implementation.
|
|
||||||
type Context struct {
|
|
||||||
context.Context
|
|
||||||
|
|
||||||
// User is the user making the request
|
|
||||||
User user.Info
|
|
||||||
// Authorizer helps you determines if a user is authorized to perform
|
|
||||||
// actions to specific resources.
|
|
||||||
Authorizer authorizer.Authorizer
|
|
||||||
// GroupVersionResource is the GVR of the request.
|
|
||||||
// It makes it easy to create errors such as in:
|
|
||||||
// apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
|
||||||
GroupVersionResource schema.GroupVersionResource
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store should provide all required operations to serve a given resource. A
|
|
||||||
// resource is defined by the resource itself (T) and a list type for the resource (TList).
|
|
||||||
// For example, Store[*Token, *TokenList] is a store that allows CRUD operations on *Token
|
|
||||||
// objects and allows listing tokens in a *TokenList object.
|
|
||||||
//
|
|
||||||
// Store does not define the backing storage for a resource. The storage is
|
|
||||||
// up to the implementer. For example, resources could be stored in another ETCD
|
|
||||||
// database, in a SQLite database, in another built-in resource such as Secrets.
|
|
||||||
// It is also possible to have no storage at all.
|
|
||||||
//
|
|
||||||
// Errors returned by the Store should use errors from k8s.io/apimachinery/pkg/api/errors. This
|
|
||||||
// will ensure that the right error will be returned to the clients (eg: kubectl, client-go) so
|
|
||||||
// they can react accordingly. For example, if an object is not found, store should
|
|
||||||
// return the following error:
|
|
||||||
//
|
|
||||||
// apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
|
||||||
//
|
|
||||||
// Stores should make use of the various metav1.*Options as best as possible.
|
|
||||||
// Those options are the same options coming from client-go or kubectl, generally
|
|
||||||
// meant to control the behavior of the stores. Note: We currently don't have
|
|
||||||
// field-manager enabled.
|
|
||||||
type Store[T runtime.Object, TList runtime.Object] interface {
|
|
||||||
// Create should store the resource to some backing storage.
|
|
||||||
//
|
|
||||||
// It can apply modifications as necessary before storing it. It must
|
|
||||||
// return a resource of the type of the store, but can
|
|
||||||
// create/update/delete arbitrary objects in Kubernetes without
|
|
||||||
// returning them to the user.
|
|
||||||
//
|
|
||||||
// It is called either when a request creates a resource, or when a
|
|
||||||
// request updates a resource that doesn't exist.
|
|
||||||
Create(ctx Context, obj T, opts *metav1.CreateOptions) (T, error)
|
|
||||||
// Update should overwrite a resource that is present in the backing storage.
|
|
||||||
//
|
|
||||||
// It can apply modifications as necessary before storing it. It must
|
|
||||||
// return a resource of the type of the store, but can
|
|
||||||
// create/update/delete arbitrary objects in Kubernetes without
|
|
||||||
// returning them to the user.
|
|
||||||
//
|
|
||||||
// It is called when a request updates a resource (eg: through a patch or update request)
|
|
||||||
Update(ctx Context, obj T, opts *metav1.UpdateOptions) (T, error)
|
|
||||||
// Get retrieves the resource with the given name from the backing storage.
|
|
||||||
//
|
|
||||||
// Get is called for the following requests:
|
|
||||||
// - get requests: The object must be returned.
|
|
||||||
// - update requests: The object is needed to apply a JSON patch and to make some validation on the change.
|
|
||||||
// - delete requests: The object is needed to make some validation on it.
|
|
||||||
Get(ctx Context, name string, opts *metav1.GetOptions) (T, error)
|
|
||||||
// List retrieves all resources matching the given ListOptions from the backing storage.
|
|
||||||
List(ctx Context, opts *metav1.ListOptions) (TList, error)
|
|
||||||
// Watch sends change events to a returned channel.
|
|
||||||
//
|
|
||||||
// The store is responsible for closing the channel.
|
|
||||||
Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[T], error)
|
|
||||||
// Delete deletes the resource of the given name from the backing storage.
|
|
||||||
Delete(ctx Context, name string, opts *metav1.DeleteOptions) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type WatchEvent[T runtime.Object] struct {
|
|
||||||
Event watch.EventType
|
|
||||||
Object T
|
|
||||||
}
|
|
@@ -1,131 +0,0 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
|
||||||
// Source: ./pkg/ext/store.go
|
|
||||||
//
|
|
||||||
// Generated by this command:
|
|
||||||
//
|
|
||||||
// mockgen -source=./pkg/ext/store.go -destination=./pkg/ext/store_mock.go -package=ext
|
|
||||||
//
|
|
||||||
|
|
||||||
// Package ext is a generated GoMock package.
|
|
||||||
package ext
|
|
||||||
|
|
||||||
import (
|
|
||||||
reflect "reflect"
|
|
||||||
|
|
||||||
gomock "go.uber.org/mock/gomock"
|
|
||||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
||||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
// MockStore is a mock of Store interface.
|
|
||||||
type MockStore[T runtime.Object, TList runtime.Object] struct {
|
|
||||||
ctrl *gomock.Controller
|
|
||||||
recorder *MockStoreMockRecorder[T, TList]
|
|
||||||
isgomock struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MockStoreMockRecorder is the mock recorder for MockStore.
|
|
||||||
type MockStoreMockRecorder[T runtime.Object, TList runtime.Object] struct {
|
|
||||||
mock *MockStore[T, TList]
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMockStore creates a new mock instance.
|
|
||||||
func NewMockStore[T runtime.Object, TList runtime.Object](ctrl *gomock.Controller) *MockStore[T, TList] {
|
|
||||||
mock := &MockStore[T, TList]{ctrl: ctrl}
|
|
||||||
mock.recorder = &MockStoreMockRecorder[T, TList]{mock}
|
|
||||||
return mock
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXPECT returns an object that allows the caller to indicate expected use.
|
|
||||||
func (m *MockStore[T, TList]) EXPECT() *MockStoreMockRecorder[T, TList] {
|
|
||||||
return m.recorder
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create mocks base method.
|
|
||||||
func (m *MockStore[T, TList]) Create(ctx Context, obj T, opts *v1.CreateOptions) (T, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Create", ctx, obj, opts)
|
|
||||||
ret0, _ := ret[0].(T)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create indicates an expected call of Create.
|
|
||||||
func (mr *MockStoreMockRecorder[T, TList]) Create(ctx, obj, opts any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockStore[T, TList])(nil).Create), ctx, obj, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete mocks base method.
|
|
||||||
func (m *MockStore[T, TList]) Delete(ctx Context, name string, opts *v1.DeleteOptions) error {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Delete", ctx, name, opts)
|
|
||||||
ret0, _ := ret[0].(error)
|
|
||||||
return ret0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete indicates an expected call of Delete.
|
|
||||||
func (mr *MockStoreMockRecorder[T, TList]) Delete(ctx, name, opts any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore[T, TList])(nil).Delete), ctx, name, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get mocks base method.
|
|
||||||
func (m *MockStore[T, TList]) Get(ctx Context, name string, opts *v1.GetOptions) (T, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Get", ctx, name, opts)
|
|
||||||
ret0, _ := ret[0].(T)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get indicates an expected call of Get.
|
|
||||||
func (mr *MockStoreMockRecorder[T, TList]) Get(ctx, name, opts any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore[T, TList])(nil).Get), ctx, name, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List mocks base method.
|
|
||||||
func (m *MockStore[T, TList]) List(ctx Context, opts *v1.ListOptions) (TList, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "List", ctx, opts)
|
|
||||||
ret0, _ := ret[0].(TList)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// List indicates an expected call of List.
|
|
||||||
func (mr *MockStoreMockRecorder[T, TList]) List(ctx, opts any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStore[T, TList])(nil).List), ctx, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update mocks base method.
|
|
||||||
func (m *MockStore[T, TList]) Update(ctx Context, obj T, opts *v1.UpdateOptions) (T, error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Update", ctx, obj, opts)
|
|
||||||
ret0, _ := ret[0].(T)
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update indicates an expected call of Update.
|
|
||||||
func (mr *MockStoreMockRecorder[T, TList]) Update(ctx, obj, opts any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockStore[T, TList])(nil).Update), ctx, obj, opts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch mocks base method.
|
|
||||||
func (m *MockStore[T, TList]) Watch(ctx Context, opts *v1.ListOptions) (<-chan WatchEvent[T], error) {
|
|
||||||
m.ctrl.T.Helper()
|
|
||||||
ret := m.ctrl.Call(m, "Watch", ctx, opts)
|
|
||||||
ret0, _ := ret[0].(<-chan WatchEvent[T])
|
|
||||||
ret1, _ := ret[1].(error)
|
|
||||||
return ret0, ret1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Watch indicates an expected call of Watch.
|
|
||||||
func (mr *MockStoreMockRecorder[T, TList]) Watch(ctx, opts any) *gomock.Call {
|
|
||||||
mr.mock.ctrl.T.Helper()
|
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockStore[T, TList])(nil).Watch), ctx, opts)
|
|
||||||
}
|
|
199
pkg/ext/utils.go
Normal file
199
pkg/ext/utils.go
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
package ext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/registry/rest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConvertFunc will convert an object to a list of cell in a metav1.Table (think kubectl get table output)
|
||||||
|
type ConvertFunc[T runtime.Object] func(obj T) []string
|
||||||
|
|
||||||
|
// ConvertToTable helps implement [rest.Lister] and [rest.TableConvertor].
|
||||||
|
//
|
||||||
|
// It converts an object or a list of objects to a Table, which is used by kubectl
|
||||||
|
// (and Rancher UI) to display a table of the items.
|
||||||
|
func ConvertToTable[T runtime.Object](ctx context.Context, object runtime.Object, tableOptions runtime.Object, groupResource schema.GroupResource, columnDefs []metav1.TableColumnDefinition, convertFn ConvertFunc[T]) (*metav1.Table, error) {
|
||||||
|
result, err := convertToTable(ctx, object, tableOptions, groupResource, columnDefs, convertFn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, convertError(err)
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToTableDefault helps implement [rest.Lister] and [rest.TableConvertor].
|
||||||
|
//
|
||||||
|
// This uses the default table conversion that displays the following two
|
||||||
|
// columns: Name and Created At.
|
||||||
|
func ConvertToTableDefault[T runtime.Object](ctx context.Context, object runtime.Object, tableOptions runtime.Object, groupResource schema.GroupResource) (*metav1.Table, error) {
|
||||||
|
return ConvertToTable[T](ctx, object, tableOptions, groupResource, nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertToTable[T runtime.Object](ctx context.Context, object runtime.Object, tableOptions runtime.Object, groupResource schema.GroupResource, columnDefs []metav1.TableColumnDefinition, convertFn ConvertFunc[T]) (*metav1.Table, error) {
|
||||||
|
defaultTableConverter := rest.NewDefaultTableConvertor(groupResource)
|
||||||
|
table, err := defaultTableConverter.ConvertToTable(ctx, object, tableOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if columnDefs == nil {
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override only if there were definitions before (to respect the NoHeader option)
|
||||||
|
if len(table.ColumnDefinitions) > 0 {
|
||||||
|
table.ColumnDefinitions = columnDefs
|
||||||
|
}
|
||||||
|
table.Rows = []metav1.TableRow{}
|
||||||
|
fn := func(obj runtime.Object) error {
|
||||||
|
objT, ok := obj.(T)
|
||||||
|
if !ok {
|
||||||
|
var zeroT T
|
||||||
|
return fmt.Errorf("expected %T but got %T", zeroT, obj)
|
||||||
|
}
|
||||||
|
cells := convertFn(objT)
|
||||||
|
if len(cells) != len(columnDefs) {
|
||||||
|
return fmt.Errorf("defined %d columns but got %d cells", len(columnDefs), len(cells))
|
||||||
|
}
|
||||||
|
|
||||||
|
table.Rows = append(table.Rows, metav1.TableRow{
|
||||||
|
Cells: cellStringToCellAny(cells),
|
||||||
|
Object: runtime.RawExtension{Object: obj},
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case meta.IsListType(object):
|
||||||
|
if err := meta.EachListItem(object, fn); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := fn(object); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return table, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cellStringToCellAny(cells []string) []any {
|
||||||
|
var res []any
|
||||||
|
for _, cell := range cells {
|
||||||
|
res = append(res, cell)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateOrUpdate helps implement [rest.Updater] by handling most of the logic.
|
||||||
|
//
|
||||||
|
// It will call getFn to find the object. If not found, then createFn will
|
||||||
|
// be called, which should create the object. Otherwise, the updateFn will be called,
|
||||||
|
// which should update the object.
|
||||||
|
//
|
||||||
|
// createValidation is called before createFn. It will do validation such as:
|
||||||
|
// - verifying that the user is allowed to by checking for the "create" verb.
|
||||||
|
// See here for details: https://github.com/kubernetes/apiserver/blob/70ed6fdbea9eb37bd1d7558e90c20cfe888955e8/pkg/endpoints/handlers/update.go#L190-L201
|
||||||
|
// - running mutating/validating webhooks (though we're not using them yet)
|
||||||
|
//
|
||||||
|
// updateValidation is called before updateFn. It will do validation such as:
|
||||||
|
// - running mutating/validating webhooks (though we're not using them yet)
|
||||||
|
func CreateOrUpdate[T runtime.Object](
|
||||||
|
ctx context.Context,
|
||||||
|
name string,
|
||||||
|
objInfo rest.UpdatedObjectInfo,
|
||||||
|
createValidation rest.ValidateObjectFunc,
|
||||||
|
updateValidation rest.ValidateObjectUpdateFunc,
|
||||||
|
forceAllowCreate bool,
|
||||||
|
options *metav1.UpdateOptions,
|
||||||
|
getFn func(ctx context.Context, name string, opts *metav1.GetOptions) (T, error),
|
||||||
|
createFn func(ctx context.Context, obj T, opts *metav1.CreateOptions) (T, error),
|
||||||
|
updateFn func(ctx context.Context, obj T, opts *metav1.UpdateOptions) (T, error),
|
||||||
|
) (runtime.Object, bool, error) {
|
||||||
|
oldObj, err := getFn(ctx, name, &metav1.GetOptions{})
|
||||||
|
if err != nil {
|
||||||
|
if !apierrors.IsNotFound(err) {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
obj, err := objInfo.UpdatedObject(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, convertError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = createValidation(ctx, obj); err != nil {
|
||||||
|
return nil, false, convertError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tObj, ok := obj.(T)
|
||||||
|
if !ok {
|
||||||
|
var zeroT T
|
||||||
|
return nil, false, convertError(fmt.Errorf("object was of type %T, not of expected type %T", obj, zeroT))
|
||||||
|
}
|
||||||
|
|
||||||
|
newObj, err := createFn(ctx, tObj, &metav1.CreateOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, convertError(err)
|
||||||
|
}
|
||||||
|
return newObj, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
newObj, err := objInfo.UpdatedObject(ctx, oldObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, convertError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newT, ok := newObj.(T)
|
||||||
|
if !ok {
|
||||||
|
var zeroT T
|
||||||
|
return nil, false, convertError(fmt.Errorf("object was of type %T, not of expected type %T", newObj, zeroT))
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateValidation != nil {
|
||||||
|
err = updateValidation(ctx, newT, oldObj)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, convertError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newT, err = updateFn(ctx, newT, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return newT, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertListOptions converts an internal ListOptions to one used by client-go.
|
||||||
|
//
|
||||||
|
// This can be useful if wrapping Watch or List methods to client-go's equivalent.
|
||||||
|
func ConvertListOptions(options *metainternalversion.ListOptions) (*metav1.ListOptions, error) {
|
||||||
|
scheme := sync.OnceValue(func() *runtime.Scheme {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
metainternalversion.AddToScheme(scheme)
|
||||||
|
return scheme
|
||||||
|
})()
|
||||||
|
|
||||||
|
var out metav1.ListOptions
|
||||||
|
err := scheme.Convert(options, &out, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("converting list options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertError(err error) error {
|
||||||
|
if _, ok := err.(apierrors.APIStatus); ok {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return apierrors.NewInternalError(err)
|
||||||
|
}
|
69
pkg/ext/utils_test.go
Normal file
69
pkg/ext/utils_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package ext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConvertListOptions(t *testing.T) {
|
||||||
|
internal := &metainternalversion.ListOptions{
|
||||||
|
ResourceVersion: "foo",
|
||||||
|
Watch: true,
|
||||||
|
}
|
||||||
|
expected := &metav1.ListOptions{
|
||||||
|
ResourceVersion: "foo",
|
||||||
|
Watch: true,
|
||||||
|
}
|
||||||
|
got, err := ConvertListOptions(internal)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConvertError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input error
|
||||||
|
output error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "api status error",
|
||||||
|
input: &apierrors.StatusError{
|
||||||
|
ErrStatus: metav1.Status{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Reason: metav1.StatusReasonNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: &apierrors.StatusError{
|
||||||
|
ErrStatus: metav1.Status{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
Reason: metav1.StatusReasonNotFound,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "generic error",
|
||||||
|
input: assert.AnError,
|
||||||
|
output: &apierrors.StatusError{ErrStatus: metav1.Status{
|
||||||
|
Status: metav1.StatusFailure,
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
Reason: metav1.StatusReasonInternalError,
|
||||||
|
Details: &metav1.StatusDetails{
|
||||||
|
Causes: []metav1.StatusCause{{Message: assert.AnError.Error()}},
|
||||||
|
},
|
||||||
|
Message: fmt.Sprintf("Internal error occurred: %v", assert.AnError),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.output, convertError(tt.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user