mirror of
https://github.com/rancher/steve.git
synced 2025-07-04 02:26:36 +00:00
200 lines
6.2 KiB
Go
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)
|
||
|
}
|