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"
|
||||
"sync"
|
||||
|
||||
"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"
|
||||
@@ -72,11 +71,11 @@ type ExtensionAPIServerOptions struct {
|
||||
//
|
||||
// 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
|
||||
// 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
|
||||
// can expect the following requests from the main kube-apiserver:
|
||||
//
|
||||
@@ -201,34 +200,49 @@ func (s *ExtensionAPIServer) ServeHTTP(w http.ResponseWriter, req *http.Request)
|
||||
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.
|
||||
//
|
||||
// Here's an example store for a Token and TokenList resource in the ext.cattle.io/v1 apiVersion:
|
||||
//
|
||||
// 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)
|
||||
// This can be used to inject the authorizer in stores that need them.
|
||||
func (s *ExtensionAPIServer) GetAuthorizer() authorizer.Authorizer {
|
||||
return s.authorizer
|
||||
}
|
||||
|
||||
// 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]
|
||||
if !ok {
|
||||
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)
|
||||
}
|
||||
|
||||
del := &delegateError[T, TList]{
|
||||
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
|
||||
apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = storage
|
||||
s.apiGroups[gvk.Group] = apiGroup
|
||||
return nil
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
package ext
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -12,22 +13,32 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
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/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
)
|
||||
|
||||
var _ rest.Storage = (*authnTestStore)(nil)
|
||||
var _ rest.Lister = (*authnTestStore)(nil)
|
||||
|
||||
type authnTestStore struct {
|
||||
*testStore
|
||||
*testStore[*TestType, *TestTypeList]
|
||||
userCh chan user.Info
|
||||
}
|
||||
|
||||
func (t *authnTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
||||
t.userCh <- ctx.User
|
||||
func (t *authnTestStore) List(ctx context.Context, _ *metainternalversion.ListOptions) (runtime.Object, error) {
|
||||
userInfo, ok := request.UserFrom(ctx)
|
||||
if !ok {
|
||||
return nil, convertError(fmt.Errorf("missing user info"))
|
||||
}
|
||||
|
||||
t.userCh <- userInfo
|
||||
return &testTypeListFixture, nil
|
||||
}
|
||||
|
||||
@@ -50,10 +61,10 @@ func TestAuthenticationCustom(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
store := &authnTestStore{
|
||||
testStore: &testStore{},
|
||||
testStore: newDefaultTestStore(),
|
||||
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.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||
|
@@ -23,32 +23,40 @@ import (
|
||||
"go.uber.org/mock/gomock"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
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/serializer"
|
||||
yamlutil "k8s.io/apimachinery/pkg/util/yaml"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/endpoints/request"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/apiserver/pkg/server/options"
|
||||
)
|
||||
|
||||
type authzTestStore struct {
|
||||
*testStore
|
||||
*testStore[*TestType, *TestTypeList]
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
func (t *authzTestStore) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestType, error) {
|
||||
if name == "not-found" {
|
||||
return nil, apierrors.NewNotFound(ctx.GroupVersionResource.GroupResource(), name)
|
||||
}
|
||||
return t.testStore.Get(ctx, name, opts)
|
||||
// Get implements [rest.Getter]
|
||||
func (t *authzTestStore) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
|
||||
return t.get(ctx, name, options)
|
||||
}
|
||||
|
||||
func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
||||
if ctx.User.GetName() == "read-only-error" {
|
||||
decision, _, err := ctx.Authorizer.Authorize(ctx, authorizer.AttributesRecord{
|
||||
User: ctx.User,
|
||||
// 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"))
|
||||
}
|
||||
|
||||
if userInfo.GetName() == "read-only-error" {
|
||||
decision, _, err := t.authorizer.Authorize(ctx, authorizer.AttributesRecord{
|
||||
User: userInfo,
|
||||
Verb: "customverb",
|
||||
Resource: "testtypes",
|
||||
ResourceRequest: true,
|
||||
@@ -58,7 +66,7 @@ func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeL
|
||||
if err == nil {
|
||||
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.APIVersion = "v1"
|
||||
return nil, forbidden
|
||||
@@ -67,6 +75,54 @@ func (t *authzTestStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeL
|
||||
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() {
|
||||
t := s.T()
|
||||
|
||||
@@ -89,10 +145,7 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
||||
ln, _, err := options.CreateListener("", ":0", net.ListenConfig{})
|
||||
require.NoError(t, err)
|
||||
|
||||
store := &authzTestStore{
|
||||
testStore: &testStore{},
|
||||
}
|
||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
||||
extensionAPIServer, cleanup, err := setupExtensionAPIServerNoStore(t, scheme, func(opts *ExtensionAPIServerOptions) {
|
||||
opts.Listener = ln
|
||||
opts.Authorizer = authz
|
||||
opts.Authenticator = authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) {
|
||||
@@ -104,7 +157,17 @@ func (s *ExtensionAPIServerSuite) TestAuthorization() {
|
||||
User: user,
|
||||
}, 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)
|
||||
defer cleanup()
|
||||
|
||||
|
@@ -11,11 +11,11 @@ import (
|
||||
"net/http/httptest"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
regrest "k8s.io/apiserver/pkg/registry/rest"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/rest"
|
||||
)
|
||||
@@ -41,83 +42,6 @@ func authzAllowAll(ctx context.Context, a authorizer.Attributes) (authorizer.Dec
|
||||
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) {
|
||||
scheme := runtime.NewScheme()
|
||||
AddToScheme(scheme)
|
||||
@@ -128,8 +52,10 @@ func TestStore(t *testing.T) {
|
||||
ln, err := (&net.ListenConfig{}).Listen(ctx, "tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
store := newMapStore()
|
||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
||||
store := newDefaultTestStore()
|
||||
store.items = make(map[string]*TestType)
|
||||
|
||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, store, func(opts *ExtensionAPIServerOptions) {
|
||||
opts.Listener = ln
|
||||
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
||||
@@ -298,36 +224,48 @@ func TestStore(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
var _ Store[*TestTypeOther, *TestTypeOtherList] = (*testStoreOther)(nil)
|
||||
|
||||
// This store is meant to be able to test many stores
|
||||
type testStoreOther struct {
|
||||
// This store tests when there's only a subset of verbs supported
|
||||
type partialStorage struct {
|
||||
gvk schema.GroupVersionKind
|
||||
}
|
||||
|
||||
func (t *testStoreOther) Create(ctx Context, obj *TestTypeOther, opts *metav1.CreateOptions) (*TestTypeOther, error) {
|
||||
return &testTypeOtherFixture, nil
|
||||
// New implements [regrest.Storage]
|
||||
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) {
|
||||
return &testTypeOtherFixture, nil
|
||||
// Destroy implements [regrest.Storage]
|
||||
func (t *partialStorage) Destroy() {
|
||||
}
|
||||
|
||||
func (t *testStoreOther) Get(ctx Context, name string, opts *metav1.GetOptions) (*TestTypeOther, error) {
|
||||
return &testTypeOtherFixture, nil
|
||||
// GetSingularName implements [regrest.SingularNameProvider]
|
||||
func (t *partialStorage) GetSingularName() string {
|
||||
return "testtype"
|
||||
}
|
||||
|
||||
func (t *testStoreOther) List(ctx Context, opts *metav1.ListOptions) (*TestTypeOtherList, error) {
|
||||
return &testTypeOtherListFixture, nil
|
||||
// NamespaceScoped implements [regrest.Scoper]
|
||||
func (t *partialStorage) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
func (t *testStoreOther) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestTypeOther], error) {
|
||||
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
|
||||
// testing this here
|
||||
func TestDiscoveryAndOpenAPI(t *testing.T) {
|
||||
@@ -343,35 +281,67 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
||||
Group: "ext2.cattle.io",
|
||||
Version: "v3",
|
||||
}
|
||||
|
||||
partialGroupVersion := schema.GroupVersion{
|
||||
Group: "ext.cattle.io",
|
||||
Version: "v4",
|
||||
}
|
||||
scheme.AddKnownTypes(differentVersion, &TestType{}, &TestTypeList{})
|
||||
scheme.AddKnownTypes(differentGroupVersion, &TestType{}, &TestTypeList{})
|
||||
scheme.AddKnownTypes(partialGroupVersion, &TestType{}, &TestTypeList{})
|
||||
metav1.AddToGroupVersion(scheme, differentVersion)
|
||||
metav1.AddToGroupVersion(scheme, differentGroupVersion)
|
||||
metav1.AddToGroupVersion(scheme, partialGroupVersion)
|
||||
|
||||
ln, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", ":0")
|
||||
require.NoError(t, err)
|
||||
|
||||
store := &testStore{}
|
||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, &TestType{}, &TestTypeList{}, store, func(opts *ExtensionAPIServerOptions) {
|
||||
store := newDefaultTestStore()
|
||||
extensionAPIServer, cleanup, err := setupExtensionAPIServer(t, scheme, store, func(opts *ExtensionAPIServerOptions) {
|
||||
opts.Listener = ln
|
||||
opts.Authorizer = authorizer.AuthorizerFunc(authzAllowAll)
|
||||
opts.Authenticator = authenticator.RequestFunc(authAsAdmin)
|
||||
}, func(s *ExtensionAPIServer) error {
|
||||
store := &testStoreOther{}
|
||||
err := InstallStore(s, &TestTypeOther{}, &TestTypeOtherList{}, "testtypeothers", "testtypeother", testTypeGV.WithKind("TestTypeOther"), store)
|
||||
err = s.Install("testtypeothers", testTypeGV.WithKind("TestTypeOther"), &testStore[*TestTypeOther, *TestTypeOtherList]{
|
||||
singular: "testtypeother",
|
||||
objT: &TestTypeOther{},
|
||||
objListT: &TestTypeOtherList{},
|
||||
gvk: testTypeGV.WithKind("TestTypeOther"),
|
||||
gvr: schema.GroupVersionResource{Group: testTypeGV.Group, Version: testTypeGV.Version, Resource: "testtypes"},
|
||||
})
|
||||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
err = s.Install("testtypes", partialGroupVersion.WithKind("TestType"), &partialStorage{
|
||||
gvk: partialGroupVersion.WithKind("TestType"),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
@@ -401,6 +371,10 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
||||
{
|
||||
Name: "ext.cattle.io",
|
||||
Versions: []metav1.GroupVersionForDiscovery{
|
||||
{
|
||||
GroupVersion: "ext.cattle.io/v4",
|
||||
Version: "v4",
|
||||
},
|
||||
{
|
||||
GroupVersion: "ext.cattle.io/v2",
|
||||
Version: "v2",
|
||||
@@ -450,6 +424,10 @@ func TestDiscoveryAndOpenAPI(t *testing.T) {
|
||||
GroupVersion: "ext.cattle.io/v2",
|
||||
Version: "v2",
|
||||
},
|
||||
{
|
||||
GroupVersion: "ext.cattle.io/v4",
|
||||
Version: "v4",
|
||||
},
|
||||
{
|
||||
GroupVersion: "ext.cattle.io/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",
|
||||
expectedStatusCode: http.StatusOK,
|
||||
@@ -664,15 +668,29 @@ func TestNoStore(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func setupExtensionAPIServer[
|
||||
T runtime.Object,
|
||||
TList runtime.Object,
|
||||
](
|
||||
func setupExtensionAPIServer(
|
||||
t *testing.T,
|
||||
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,
|
||||
scheme *runtime.Scheme,
|
||||
objT T,
|
||||
objTList TList,
|
||||
store Store[T, TList],
|
||||
optionSetter func(*ExtensionAPIServerOptions),
|
||||
extensionAPIServerSetter func(*ExtensionAPIServer) error,
|
||||
) (*ExtensionAPIServer, func(), error) {
|
||||
@@ -694,11 +712,6 @@ func setupExtensionAPIServer[
|
||||
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 {
|
||||
err = extensionAPIServerSetter(extensionAPIServer)
|
||||
if err != nil {
|
||||
@@ -768,3 +781,222 @@ func createRecordingWatcher(scheme *runtime.Scheme, gvr schema.GroupVersionResou
|
||||
stop: myWatch.Stop,
|
||||
}, 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
|
||||
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/watch"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
common "k8s.io/kube-openapi/pkg/common"
|
||||
spec "k8s.io/kube-openapi/pkg/validation/spec"
|
||||
@@ -162,33 +170,239 @@ func (t *TestTypeOther) DeepCopyObject() runtime.Object {
|
||||
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) {
|
||||
return &testTypeFixture, nil
|
||||
func newDefaultTestStore() *testStore[*TestType, *TestTypeList] {
|
||||
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) {
|
||||
return &testTypeFixture, nil
|
||||
// New implements [rest.Storage]
|
||||
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) {
|
||||
return &testTypeFixture, nil
|
||||
// GetSingularName implements [rest.SingularNameProvider]
|
||||
func (t *testStore[T, TList]) GetSingularName() string {
|
||||
return t.singular
|
||||
}
|
||||
|
||||
func (t *testStore) List(ctx Context, opts *metav1.ListOptions) (*TestTypeList, error) {
|
||||
return &testTypeListFixture, nil
|
||||
// NamespaceScoped implements [rest.Scoper]
|
||||
func (t *testStore[T, TList]) NamespaceScoped() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *testStore) Watch(ctx Context, opts *metav1.ListOptions) (<-chan WatchEvent[*TestType], error) {
|
||||
return nil, nil
|
||||
// GroupVersionKind implements [rest.GroupVersionKindProvider]
|
||||
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 {
|
||||
return nil
|
||||
// Destroy implements [rest.Storage]
|
||||
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.
|
||||
@@ -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