define table converter for cluster api that exposes more than name/creation timestamp

Signed-off-by: carlory <baofa.fan@daocloud.io>
This commit is contained in:
carlory 2021-12-28 16:29:38 +08:00
parent f5c33bbfff
commit e69b58c981
7 changed files with 379 additions and 6 deletions

View File

@ -10,10 +10,6 @@ import (
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:scope="Cluster"
// +kubebuilder:subresource:status
// +kubebuilder:printcolumn:JSONPath=`.status.kubernetesVersion`,name="Version",type=string
// +kubebuilder:printcolumn:JSONPath=`.spec.syncMode`,name="Mode",type=string
// +kubebuilder:printcolumn:JSONPath=`.status.conditions[?(@.type=="Ready")].status`,name="Ready",type=string
// +kubebuilder:printcolumn:JSONPath=`.metadata.creationTimestamp`,name="Age",type=date
// Cluster represents the desire state and status of a member cluster.
type Cluster struct {

37
pkg/printers/interface.go Normal file
View File

@ -0,0 +1,37 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package printers
import (
"io"
"k8s.io/apimachinery/pkg/runtime"
)
// ResourcePrinter is an interface that knows how to print runtime objects.
type ResourcePrinter interface {
// Print receives a runtime object, formats it and prints it to a writer.
PrintObj(runtime.Object, io.Writer) error
}
// ResourcePrinterFunc is a function that can print objects
type ResourcePrinterFunc func(runtime.Object, io.Writer) error
// PrintObj implements ResourcePrinter
func (fn ResourcePrinterFunc) PrintObj(obj runtime.Object, w io.Writer) error {
return fn(obj, w)
}

View File

@ -0,0 +1,70 @@
package internalversion
import (
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/duration"
clusterapis "github.com/karmada-io/karmada/pkg/apis/cluster"
"github.com/karmada-io/karmada/pkg/printers"
)
// AddHandlers adds print handlers for default Karmada types dealing with internal versions.
func AddHandlers(h printers.PrintHandler) {
clusterColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Version", Type: "string", Description: "KubernetesVersion represents version of the member cluster."},
{Name: "Mode", Type: "string", Description: "SyncMode describes how a cluster sync resources from karmada control plane."},
{Name: "Ready", Type: "string", Description: "The aggregate readiness state of this cluster for accepting workloads."},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
// ignore errors because we enable errcheck golangci-lint.
_ = h.TableHandler(clusterColumnDefinitions, printClusterList)
_ = h.TableHandler(clusterColumnDefinitions, printCluster)
}
func printClusterList(clusterList *clusterapis.ClusterList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(clusterList.Items))
for i := range clusterList.Items {
r, err := printCluster(&clusterList.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printCluster(cluster *clusterapis.Cluster, options printers.GenerateOptions) ([]metav1.TableRow, error) {
ready := "Unknown"
for _, condition := range cluster.Status.Conditions {
if condition.Type == clusterapis.ClusterConditionReady {
ready = string(condition.Status)
break
}
}
row := metav1.TableRow{
Object: runtime.RawExtension{Object: cluster},
}
row.Cells = append(
row.Cells,
cluster.Name,
cluster.Status.KubernetesVersion,
cluster.Spec.SyncMode,
ready,
translateTimestampSince(cluster.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
// translateTimestampSince returns the elapsed time since timestamp in
// human-readable approximation.
func translateTimestampSince(timestamp metav1.Time) string {
if timestamp.IsZero() {
return "<unknown>"
}
return duration.HumanDuration(time.Since(timestamp.Time))
}

View File

@ -0,0 +1,49 @@
package internalversion
import (
"reflect"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/diff"
clusterapis "github.com/karmada-io/karmada/pkg/apis/cluster"
"github.com/karmada-io/karmada/pkg/printers"
)
func TestPrintCluster(t *testing.T) {
tests := []struct {
cluster clusterapis.Cluster
expect []metav1.TableRow
}{
// Test name, kubernetes version, sync mode, cluster ready status,
{
clusterapis.Cluster{
ObjectMeta: metav1.ObjectMeta{Name: "test1"},
Spec: clusterapis.ClusterSpec{
SyncMode: clusterapis.Push,
},
Status: clusterapis.ClusterStatus{
KubernetesVersion: "1.21.7",
Conditions: []metav1.Condition{
{Type: clusterapis.ClusterConditionReady, Status: metav1.ConditionTrue},
},
},
},
[]metav1.TableRow{{Cells: []interface{}{"test1", "1.21.7", clusterapis.ClusterSyncMode("Push"), "True", "<unknown>"}}},
},
}
for i, test := range tests {
rows, err := printCluster(&test.cluster, printers.GenerateOptions{})
if err != nil {
t.Fatal(err)
}
for i := range rows {
rows[i].Object.Object = nil
}
if !reflect.DeepEqual(test.expect, rows) {
t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expect, rows))
}
}
}

View File

@ -0,0 +1,48 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package storage
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"github.com/karmada-io/karmada/pkg/printers"
)
// TableConvertor struct - converts objects to metav1.Table using printers.TableGenerator
type TableConvertor struct {
printers.TableGenerator
}
// ConvertToTable method - converts objects to metav1.Table objects using TableGenerator
func (c TableConvertor) ConvertToTable(ctx context.Context, obj runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
noHeaders := false
if tableOptions != nil {
switch t := tableOptions.(type) {
case *metav1.TableOptions:
if t != nil {
noHeaders = t.NoHeaders
}
default:
return nil, fmt.Errorf("unrecognized type %T for table options, can't display tabular output", tableOptions)
}
}
return c.TableGenerator.GenerateTable(obj, printers.GenerateOptions{Wide: true, NoHeaders: noHeaders})
}

View File

@ -0,0 +1,171 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package printers
import (
"fmt"
"reflect"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
)
// GenerateOptions encapsulates attributes for table generation.
type GenerateOptions struct {
NoHeaders bool
Wide bool
}
// TableGenerator - an interface for generating metav1.Table provided a runtime.Object
type TableGenerator interface {
GenerateTable(obj runtime.Object, options GenerateOptions) (*metav1.Table, error)
}
// PrintHandler - interface to handle printing provided an array of metav1.TableColumnDefinition
type PrintHandler interface {
TableHandler(columns []metav1.TableColumnDefinition, printFunc interface{}) error
}
type handlerEntry struct {
columnDefinitions []metav1.TableColumnDefinition
printFunc reflect.Value
}
// HumanReadableGenerator is an implementation of TableGenerator used to generate
// a table for a specific resource. The table is printed with a TablePrinter using
// PrintObj().
type HumanReadableGenerator struct {
handlerMap map[reflect.Type]*handlerEntry
}
var _ TableGenerator = &HumanReadableGenerator{}
var _ PrintHandler = &HumanReadableGenerator{}
// NewTableGenerator creates a HumanReadableGenerator suitable for calling GenerateTable().
func NewTableGenerator() *HumanReadableGenerator {
return &HumanReadableGenerator{
handlerMap: make(map[reflect.Type]*handlerEntry),
}
}
// With method - accepts a list of builder functions that modify HumanReadableGenerator
func (h *HumanReadableGenerator) With(fns ...func(PrintHandler)) *HumanReadableGenerator {
for _, fn := range fns {
fn(h)
}
return h
}
// GenerateTable returns a table for the provided object, using the printer registered for that type. It returns
// a table that includes all of the information requested by options, but will not remove rows or columns. The
// caller is responsible for applying rules related to filtering rows or columns.
func (h *HumanReadableGenerator) GenerateTable(obj runtime.Object, options GenerateOptions) (*metav1.Table, error) {
t := reflect.TypeOf(obj)
handler, ok := h.handlerMap[t]
if !ok {
return nil, fmt.Errorf("no table handler registered for this type %v", t)
}
args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)}
results := handler.printFunc.Call(args)
if !results[1].IsNil() {
return nil, results[1].Interface().(error)
}
var columns []metav1.TableColumnDefinition
if !options.NoHeaders {
columns = handler.columnDefinitions
if !options.Wide {
columns = make([]metav1.TableColumnDefinition, 0, len(handler.columnDefinitions))
for i := range handler.columnDefinitions {
if handler.columnDefinitions[i].Priority != 0 {
continue
}
columns = append(columns, handler.columnDefinitions[i])
}
}
}
table := &metav1.Table{
ListMeta: metav1.ListMeta{
ResourceVersion: "",
},
ColumnDefinitions: columns,
Rows: results[0].Interface().([]metav1.TableRow),
}
if m, err := meta.ListAccessor(obj); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
table.Continue = m.GetContinue()
table.RemainingItemCount = m.GetRemainingItemCount()
} else {
if m, err := meta.CommonAccessor(obj); err == nil {
table.ResourceVersion = m.GetResourceVersion()
table.SelfLink = m.GetSelfLink()
}
}
return table, nil
}
// TableHandler adds a print handler with a given set of columns to HumanReadableGenerator instance.
// See ValidateRowPrintHandlerFunc for required method signature.
func (h *HumanReadableGenerator) TableHandler(columnDefinitions []metav1.TableColumnDefinition, printFunc interface{}) error {
printFuncValue := reflect.ValueOf(printFunc)
if err := ValidateRowPrintHandlerFunc(printFuncValue); err != nil {
utilruntime.HandleError(fmt.Errorf("unable to register print function: %v", err))
return err
}
entry := &handlerEntry{
columnDefinitions: columnDefinitions,
printFunc: printFuncValue,
}
objType := printFuncValue.Type().In(0)
if _, ok := h.handlerMap[objType]; ok {
err := fmt.Errorf("registered duplicate printer for %v", objType)
utilruntime.HandleError(err)
return err
}
h.handlerMap[objType] = entry
return nil
}
// ValidateRowPrintHandlerFunc validates print handler signature.
// printFunc is the function that will be called to print an object.
// It must be of the following type:
// func printFunc(object ObjectType, options GenerateOptions) ([]metav1.TableRow, error)
// where ObjectType is the type of the object that will be printed, and the first
// return value is an array of rows, with each row containing a number of cells that
// match the number of columns defined for that printer function.
func ValidateRowPrintHandlerFunc(printFunc reflect.Value) error {
if printFunc.Kind() != reflect.Func {
return fmt.Errorf("invalid print handler. %#v is not a function", printFunc)
}
funcType := printFunc.Type()
if funcType.NumIn() != 2 || funcType.NumOut() != 2 {
return fmt.Errorf("invalid print handler." +
"Must accept 2 parameters and return 2 value")
}
if funcType.In(1) != reflect.TypeOf((*GenerateOptions)(nil)).Elem() ||
funcType.Out(0) != reflect.TypeOf((*[]metav1.TableRow)(nil)).Elem() ||
funcType.Out(1) != reflect.TypeOf((*error)(nil)).Elem() {
return fmt.Errorf("invalid print handler. The expected signature is: "+
"func handler(obj %v, options GenerateOptions) ([]metav1.TableRow, error)", funcType.In(0))
}
return nil
}

View File

@ -17,6 +17,9 @@ import (
clusterapis "github.com/karmada-io/karmada/pkg/apis/cluster"
karmadaclientset "github.com/karmada-io/karmada/pkg/generated/clientset/versioned"
"github.com/karmada-io/karmada/pkg/printers"
printersinternal "github.com/karmada-io/karmada/pkg/printers/internalversion"
printerstorage "github.com/karmada-io/karmada/pkg/printers/storage"
clusterregistry "github.com/karmada-io/karmada/pkg/registry/cluster"
)
@ -60,8 +63,7 @@ func NewREST(scheme *runtime.Scheme, optsGetter generic.RESTOptionsGetter) (*RES
UpdateStrategy: strategy,
DeleteStrategy: strategy,
// TODO: define table converter that exposes more than name/creation timestamp
TableConvertor: rest.NewDefaultTableConvertor(clusterapis.Resource("clusters")),
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter, AttrFunc: clusterregistry.GetAttrs}