1
0
mirror of https://github.com/rancher/steve.git synced 2025-07-04 02:26:36 +00:00
steve/pkg/ext/utils.go
Tom Lebreux fdf2ef8e93
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
2025-01-15 12:41:44 -05:00

200 lines
6.2 KiB
Go

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)
}