From 801c39b4781c8ef8e2334ba1ecd2b50dc1ffbd8f Mon Sep 17 00:00:00 2001 From: Yuvaraj Kakaraparthi Date: Tue, 6 Jul 2021 08:42:12 -0700 Subject: [PATCH] kubectl: API changes to support --subresource in kubectl Signed-off-by: Madhav Jivrajani Co-authored-by: Nikhita Raghunath Co-authored-by: Yuvaraj Kakaraparthi --- pkg/printers/internalversion/printers.go | 16 ++ pkg/printers/internalversion/printers_test.go | 37 +++ .../storageversion/storage/storage.go | 4 + .../apps/daemonset/storage/storage.go | 4 + .../apps/deployment/storage/storage.go | 8 + .../apps/replicaset/storage/storage.go | 8 + .../apps/statefulset/storage/storage.go | 8 + .../storage/storage.go | 4 + pkg/registry/batch/cronjob/storage/storage.go | 4 + pkg/registry/batch/job/storage/storage.go | 4 + .../certificates/storage/storage.go | 4 + .../core/namespace/storage/storage.go | 5 + pkg/registry/core/node/storage/storage.go | 4 + .../core/persistentvolume/storage/storage.go | 4 + .../persistentvolumeclaim/storage/storage.go | 4 + pkg/registry/core/pod/storage/storage.go | 4 + .../replicationcontroller/storage/storage.go | 8 + .../core/resourcequota/storage/storage.go | 4 + pkg/registry/core/service/storage/storage.go | 4 + .../flowcontrol/flowschema/storage/storage.go | 4 + .../storage/storage.go | 4 + .../networking/ingress/storage/storage.go | 4 + .../poddisruptionbudget/storage/storage.go | 4 + .../volumeattachment/storage/storage.go | 4 + .../pkg/apiserver/customresource_handler.go | 9 +- .../pkg/apiserver/helpers.go | 37 +++ .../tableconvertor/tableconvertor.go | 11 +- test/integration/apiserver/apiserver_test.go | 224 ++++++++++++++++++ 28 files changed, 437 insertions(+), 2 deletions(-) diff --git a/pkg/printers/internalversion/printers.go b/pkg/printers/internalversion/printers.go index 857d20d17e2..b50cd8fd3a5 100644 --- a/pkg/printers/internalversion/printers.go +++ b/pkg/printers/internalversion/printers.go @@ -27,6 +27,7 @@ import ( apiserverinternalv1alpha1 "k8s.io/api/apiserverinternal/v1alpha1" appsv1beta1 "k8s.io/api/apps/v1beta1" + autoscalingv1 "k8s.io/api/autoscaling/v1" autoscalingv2beta1 "k8s.io/api/autoscaling/v2beta1" batchv1 "k8s.io/api/batch/v1" batchv1beta1 "k8s.io/api/batch/v1beta1" @@ -589,6 +590,13 @@ func AddHandlers(h printers.PrintHandler) { } h.TableHandler(storageVersionColumnDefinitions, printStorageVersion) h.TableHandler(storageVersionColumnDefinitions, printStorageVersionList) + + scaleColumnDefinitions := []metav1.TableColumnDefinition{ + {Name: "Name", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]}, + {Name: "Desired", Type: "integer", Description: autoscalingv1.ScaleSpec{}.SwaggerDoc()["replicas"]}, + {Name: "Available", Type: "integer", Description: autoscalingv1.ScaleStatus{}.SwaggerDoc()["replicas"]}, + } + h.TableHandler(scaleColumnDefinitions, printScale) } // Pass ports=nil for all ports. @@ -2622,6 +2630,14 @@ func printPriorityLevelConfigurationList(list *flowcontrol.PriorityLevelConfigur return rows, nil } +func printScale(obj *autoscaling.Scale, options printers.GenerateOptions) ([]metav1.TableRow, error) { + row := metav1.TableRow{ + Object: runtime.RawExtension{Object: obj}, + } + row.Cells = append(row.Cells, obj.Name, obj.Spec.Replicas, obj.Status.Replicas) + return []metav1.TableRow{row}, nil +} + func printBoolPtr(value *bool) string { if value != nil { return printBool(*value) diff --git a/pkg/printers/internalversion/printers_test.go b/pkg/printers/internalversion/printers_test.go index ee8a215cf69..5736f25a9c6 100644 --- a/pkg/printers/internalversion/printers_test.go +++ b/pkg/printers/internalversion/printers_test.go @@ -5823,3 +5823,40 @@ func TestPrintStorageVersion(t *testing.T) { } } } + +func TestPrintScale(t *testing.T) { + tests := []struct { + scale autoscaling.Scale + options printers.GenerateOptions + expected []metav1.TableRow + }{ + { + scale: autoscaling.Scale{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-autoscaling", + CreationTimestamp: metav1.Time{Time: time.Now().Add(1.9e9)}, + }, + Spec: autoscaling.ScaleSpec{Replicas: 2}, + Status: autoscaling.ScaleStatus{Replicas: 1}, + }, + expected: []metav1.TableRow{ + { + Cells: []interface{}{"test-autoscaling", int32(2), int32(1)}, + }, + }, + }, + } + + for i, test := range tests { + rows, err := printScale(&test.scale, test.options) + if err != nil { + t.Fatal(err) + } + for i := range rows { + rows[i].Object.Object = nil + } + if !reflect.DeepEqual(test.expected, rows) { + t.Errorf("%d mismatch: %s", i, diff.ObjectReflectDiff(test.expected, rows)) + } + } +} diff --git a/pkg/registry/apiserverinternal/storageversion/storage/storage.go b/pkg/registry/apiserverinternal/storageversion/storage/storage.go index 3a0901391e9..af97b70af18 100644 --- a/pkg/registry/apiserverinternal/storageversion/storage/storage.go +++ b/pkg/registry/apiserverinternal/storageversion/storage/storage.go @@ -89,3 +89,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/apps/daemonset/storage/storage.go b/pkg/registry/apps/daemonset/storage/storage.go index ec337f19a77..9fdeedd27e7 100644 --- a/pkg/registry/apps/daemonset/storage/storage.go +++ b/pkg/registry/apps/daemonset/storage/storage.go @@ -111,3 +111,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/apps/deployment/storage/storage.go b/pkg/registry/apps/deployment/storage/storage.go index 773ec94b184..3fb7ccecf56 100644 --- a/pkg/registry/apps/deployment/storage/storage.go +++ b/pkg/registry/apps/deployment/storage/storage.go @@ -164,6 +164,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + // RollbackREST implements the REST endpoint for initiating the rollback of a deployment type RollbackREST struct { store *genericregistry.Store @@ -322,6 +326,10 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update return newScale, false, nil } +func (r *ScaleREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc { return func(ctx context.Context, obj runtime.Object) error { scale, err := scaleFromDeployment(obj.(*apps.Deployment)) diff --git a/pkg/registry/apps/replicaset/storage/storage.go b/pkg/registry/apps/replicaset/storage/storage.go index 11e51a300b4..5332e7cc449 100644 --- a/pkg/registry/apps/replicaset/storage/storage.go +++ b/pkg/registry/apps/replicaset/storage/storage.go @@ -160,6 +160,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + // ScaleREST implements a Scale for ReplicaSet. type ScaleREST struct { store *genericregistry.Store @@ -224,6 +228,10 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update return newScale, false, err } +func (r *ScaleREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc { return func(ctx context.Context, obj runtime.Object) error { scale, err := scaleFromReplicaSet(obj.(*apps.ReplicaSet)) diff --git a/pkg/registry/apps/statefulset/storage/storage.go b/pkg/registry/apps/statefulset/storage/storage.go index 6035350ac07..9cf19c2cf78 100644 --- a/pkg/registry/apps/statefulset/storage/storage.go +++ b/pkg/registry/apps/statefulset/storage/storage.go @@ -141,6 +141,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + // Implement ShortNamesProvider var _ rest.ShortNamesProvider = &REST{} @@ -211,6 +215,10 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update return newScale, false, err } +func (r *ScaleREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc { return func(ctx context.Context, obj runtime.Object) error { scale, err := scaleFromStatefulSet(obj.(*apps.StatefulSet)) diff --git a/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage.go b/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage.go index 8e17de50212..319d2e0ec6c 100644 --- a/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage.go +++ b/pkg/registry/autoscaling/horizontalpodautoscaler/storage/storage.go @@ -104,3 +104,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/batch/cronjob/storage/storage.go b/pkg/registry/batch/cronjob/storage/storage.go index 06724df1672..5e36416332a 100644 --- a/pkg/registry/batch/cronjob/storage/storage.go +++ b/pkg/registry/batch/cronjob/storage/storage.go @@ -102,3 +102,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/batch/job/storage/storage.go b/pkg/registry/batch/job/storage/storage.go index 2fdb108158f..ba3dd711c08 100644 --- a/pkg/registry/batch/job/storage/storage.go +++ b/pkg/registry/batch/job/storage/storage.go @@ -145,3 +145,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/certificates/certificates/storage/storage.go b/pkg/registry/certificates/certificates/storage/storage.go index 4df8f17a22a..8e2da90b16d 100644 --- a/pkg/registry/certificates/certificates/storage/storage.go +++ b/pkg/registry/certificates/certificates/storage/storage.go @@ -106,6 +106,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + var _ = rest.Patcher(&StatusREST{}) // ApprovalREST implements the REST endpoint for changing the approval state of a CSR. diff --git a/pkg/registry/core/namespace/storage/storage.go b/pkg/registry/core/namespace/storage/storage.go index 375445e3a0f..d67b15d9bd5 100644 --- a/pkg/registry/core/namespace/storage/storage.go +++ b/pkg/registry/core/namespace/storage/storage.go @@ -318,6 +318,11 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + func (r *FinalizeREST) New() runtime.Object { return r.store.New() } diff --git a/pkg/registry/core/node/storage/storage.go b/pkg/registry/core/node/storage/storage.go index 695596cde8f..54057bfb886 100644 --- a/pkg/registry/core/node/storage/storage.go +++ b/pkg/registry/core/node/storage/storage.go @@ -83,6 +83,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + // NewStorage returns a NodeStorage object that will work against nodes. func NewStorage(optsGetter generic.RESTOptionsGetter, kubeletClientConfig client.KubeletClientConfig, proxyTransport http.RoundTripper) (*NodeStorage, error) { store := &genericregistry.Store{ diff --git a/pkg/registry/core/persistentvolume/storage/storage.go b/pkg/registry/core/persistentvolume/storage/storage.go index f4f303bac99..03eaf682fe6 100644 --- a/pkg/registry/core/persistentvolume/storage/storage.go +++ b/pkg/registry/core/persistentvolume/storage/storage.go @@ -99,3 +99,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/core/persistentvolumeclaim/storage/storage.go b/pkg/registry/core/persistentvolumeclaim/storage/storage.go index f02eb05b3c8..6650606ac57 100644 --- a/pkg/registry/core/persistentvolumeclaim/storage/storage.go +++ b/pkg/registry/core/persistentvolumeclaim/storage/storage.go @@ -143,3 +143,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/core/pod/storage/storage.go b/pkg/registry/core/pod/storage/storage.go index 2cf88faaf65..3f9a7afc71f 100644 --- a/pkg/registry/core/pod/storage/storage.go +++ b/pkg/registry/core/pod/storage/storage.go @@ -286,6 +286,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + // EphemeralContainersREST implements the REST endpoint for adding EphemeralContainers type EphemeralContainersREST struct { store *genericregistry.Store diff --git a/pkg/registry/core/replicationcontroller/storage/storage.go b/pkg/registry/core/replicationcontroller/storage/storage.go index 925a308eec2..12a9f85bddf 100644 --- a/pkg/registry/core/replicationcontroller/storage/storage.go +++ b/pkg/registry/core/replicationcontroller/storage/storage.go @@ -148,6 +148,10 @@ func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + type ScaleREST struct { store *genericregistry.Store } @@ -196,6 +200,10 @@ func (r *ScaleREST) Update(ctx context.Context, name string, objInfo rest.Update return scaleFromRC(rc), false, nil } +func (r *ScaleREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + func toScaleCreateValidation(f rest.ValidateObjectFunc) rest.ValidateObjectFunc { return func(ctx context.Context, obj runtime.Object) error { return f(ctx, scaleFromRC(obj.(*api.ReplicationController))) diff --git a/pkg/registry/core/resourcequota/storage/storage.go b/pkg/registry/core/resourcequota/storage/storage.go index 961905e4832..d302169e90e 100644 --- a/pkg/registry/core/resourcequota/storage/storage.go +++ b/pkg/registry/core/resourcequota/storage/storage.go @@ -98,3 +98,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/core/service/storage/storage.go b/pkg/registry/core/service/storage/storage.go index 363cdc8f1ea..1beb2314368 100644 --- a/pkg/registry/core/service/storage/storage.go +++ b/pkg/registry/core/service/storage/storage.go @@ -178,6 +178,10 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat return r.store.Update(ctx, name, objInfo, createValidation, updateValidation, false, options) } +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} + // GetResetFields implements rest.ResetFieldsStrategy func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() diff --git a/pkg/registry/flowcontrol/flowschema/storage/storage.go b/pkg/registry/flowcontrol/flowschema/storage/storage.go index d8f14b9fd16..089b37fc46c 100644 --- a/pkg/registry/flowcontrol/flowschema/storage/storage.go +++ b/pkg/registry/flowcontrol/flowschema/storage/storage.go @@ -97,3 +97,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/flowcontrol/prioritylevelconfiguration/storage/storage.go b/pkg/registry/flowcontrol/prioritylevelconfiguration/storage/storage.go index baa80be8381..28d1f148963 100644 --- a/pkg/registry/flowcontrol/prioritylevelconfiguration/storage/storage.go +++ b/pkg/registry/flowcontrol/prioritylevelconfiguration/storage/storage.go @@ -97,3 +97,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/networking/ingress/storage/storage.go b/pkg/registry/networking/ingress/storage/storage.go index 0280c27b030..0f084232f15 100644 --- a/pkg/registry/networking/ingress/storage/storage.go +++ b/pkg/registry/networking/ingress/storage/storage.go @@ -96,3 +96,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/policy/poddisruptionbudget/storage/storage.go b/pkg/registry/policy/poddisruptionbudget/storage/storage.go index f402cfc5391..987c200d465 100644 --- a/pkg/registry/policy/poddisruptionbudget/storage/storage.go +++ b/pkg/registry/policy/poddisruptionbudget/storage/storage.go @@ -93,3 +93,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/pkg/registry/storage/volumeattachment/storage/storage.go b/pkg/registry/storage/volumeattachment/storage/storage.go index 6f8d291e1e4..cb1d0b25b53 100644 --- a/pkg/registry/storage/volumeattachment/storage/storage.go +++ b/pkg/registry/storage/volumeattachment/storage/storage.go @@ -101,3 +101,7 @@ func (r *StatusREST) Update(ctx context.Context, name string, objInfo rest.Updat func (r *StatusREST) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set { return r.store.GetResetFields() } + +func (r *StatusREST) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + return r.store.ConvertToTable(ctx, object, tableOptions) +} diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go index c317dd4d98b..d1fbf86975c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go @@ -903,7 +903,13 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd requestScopes[v.Name] = &reqScope } - // override scaleSpec subresource values + scaleColumns, err := getScaleColumnsForVersion(crd, v.Name) + if err != nil { + return nil, fmt.Errorf("the server could not properly serve the CR scale subresource columns %w", err) + } + scaleTable, _ := tableconvertor.New(scaleColumns) + + // override scale subresource values // shallow copy scaleScope := *requestScopes[v.Name] scaleConverter := scale.NewScaleConverter() @@ -916,6 +922,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd SelfLinkPathPrefix: selfLinkPrefix, SelfLinkPathSuffix: "/scale", } + scaleScope.TableConvertor = scaleTable if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) && subresources != nil && subresources.Scale != nil { scaleScope, err = scopeWithFieldManager( diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/helpers.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/helpers.go index 83ae312726f..ef18d82084e 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/helpers.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/helpers.go @@ -18,6 +18,7 @@ package apiserver import ( "fmt" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -37,6 +38,42 @@ func getColumnsForVersion(crd *apiextensionsv1.CustomResourceDefinition, version return nil, fmt.Errorf("version %s not found in apiextensionsv1.CustomResourceDefinition: %v", version, crd.Name) } +// getScaleColumnsForVersion returns 2 columns for the desired and actual number of replicas. +func getScaleColumnsForVersion(crd *apiextensionsv1.CustomResourceDefinition, version string) ([]apiextensionsv1.CustomResourceColumnDefinition, error) { + for _, v := range crd.Spec.Versions { + if version != v.Name { + continue + } + var cols []apiextensionsv1.CustomResourceColumnDefinition + if v.Subresources != nil && v.Subresources.Scale != nil { + if v.Subresources.Scale.SpecReplicasPath != "" { + cols = append(cols, apiextensionsv1.CustomResourceColumnDefinition{ + Name: "Desired", + Type: "integer", + Description: "Number of desired replicas", + JSONPath: ".spec.replicas", + }) + } + if v.Subresources.Scale.StatusReplicasPath != "" { + cols = append(cols, apiextensionsv1.CustomResourceColumnDefinition{ + Name: "Available", + Type: "integer", + Description: "Number of actual replicas", + JSONPath: ".status.replicas", + }) + } + } + cols = append(cols, apiextensionsv1.CustomResourceColumnDefinition{ + Name: "Age", + Type: "date", + Description: swaggerMetadataDescriptions["creationTimestamp"], + JSONPath: ".metadata.creationTimestamp", + }) + return cols, nil + } + return nil, fmt.Errorf("version %s not found in apiextensionsv1.CustomResourceDefinition: %v", version, crd.Name) +} + // serveDefaultColumnsIfEmpty applies logically defaulting to columns, if the input columns is empty. // NOTE: in this way, the newly logically-defaulted columns is not pointing to the original CRD object. // One cannot mutate the original CRD columns using the logically-defaulted columns. Please iterate through diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go index 0a26f114e73..b306d341cf1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/registry/customresource/tableconvertor/tableconvertor.go @@ -28,6 +28,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metatable "k8s.io/apimachinery/pkg/api/meta/table" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apiserver/pkg/registry/rest" "k8s.io/client-go/util/jsonpath" @@ -106,8 +107,16 @@ func (c *convertor) ConvertToTable(ctx context.Context, obj runtime.Object, tabl cells := make([]interface{}, 1, 1+len(c.additionalColumns)) cells[0] = name customHeaders := c.headers[1:] + us, ok := obj.(runtime.Unstructured) + if !ok { + m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + us = &unstructured.Unstructured{Object: m} + } for i, column := range c.additionalColumns { - results, err := column.FindResults(obj.(runtime.Unstructured).UnstructuredContent()) + results, err := column.FindResults(us.UnstructuredContent()) if err != nil || len(results) == 0 || len(results[0]) == 0 { cells = append(cells, nil) continue diff --git a/test/integration/apiserver/apiserver_test.go b/test/integration/apiserver/apiserver_test.go index 6ea0ede1cbf..22321972179 100644 --- a/test/integration/apiserver/apiserver_test.go +++ b/test/integration/apiserver/apiserver_test.go @@ -1369,6 +1369,226 @@ func TestAPICRDProtobuf(t *testing.T) { } } +func TestGetSubresourcesAsTables(t *testing.T) { + testNamespace := "test-transform" + tearDown, config, _, err := fixtures.StartDefaultServer(t) + if err != nil { + t.Fatal(err) + } + defer tearDown() + + s, clientset, closeFn := setup(t) + defer closeFn() + fmt.Printf("%#v\n", clientset) + + apiExtensionClient, err := apiextensionsclient.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + dynamicClient, err := dynamic.NewForConfig(config) + if err != nil { + t.Fatal(err) + } + + fooWithSubresourceCRD := &apiextensionsv1.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foosubs.cr.bar.com", + }, + Spec: apiextensionsv1.CustomResourceDefinitionSpec{ + Group: "cr.bar.com", + Scope: apiextensionsv1.NamespaceScoped, + Names: apiextensionsv1.CustomResourceDefinitionNames{ + Plural: "foosubs", + Kind: "FooSub", + }, + Versions: []apiextensionsv1.CustomResourceDefinitionVersion{ + { + Name: "v1", + Served: true, + Storage: true, + Schema: &apiextensionsv1.CustomResourceValidation{ + OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{ + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "spec": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "replicas": { + Type: "integer", + }, + }, + }, + "status": { + Type: "object", + Properties: map[string]apiextensionsv1.JSONSchemaProps{ + "replicas": { + Type: "integer", + }, + }, + }, + }, + }, + }, + Subresources: &apiextensionsv1.CustomResourceSubresources{ + Status: &apiextensionsv1.CustomResourceSubresourceStatus{}, + Scale: &apiextensionsv1.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + }, + }, + }, + }, + }, + } + + fooWithSubresourceCRD, err = fixtures.CreateNewV1CustomResourceDefinition(fooWithSubresourceCRD, apiExtensionClient, dynamicClient) + if err != nil { + t.Fatal(err) + } + subresourcesCrdGVR := schema.GroupVersionResource{Group: fooWithSubresourceCRD.Spec.Group, Version: fooWithSubresourceCRD.Spec.Versions[0].Name, Resource: "foosubs"} + subresourcesCrclient := dynamicClient.Resource(subresourcesCrdGVR).Namespace(testNamespace) + + testcases := []struct { + name string + accept string + object func(*testing.T) (metav1.Object, string, string) + subresource string + }{ + { + name: "v1 verify status subresource returns a table for CRDs", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1", + object: func(t *testing.T) (metav1.Object, string, string) { + cr, err := subresourcesCrclient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "FooSub", "metadata": map[string]interface{}{"name": "test-1"}, "spec": map[string]interface{}{"replicas": 2}}}, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create cr: %v", err) + } + return cr, subresourcesCrdGVR.Group, "foosubs" + }, + subresource: "status", + }, + { + name: "v1 verify scale subresource returns a table for CRDs", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1", + object: func(t *testing.T) (metav1.Object, string, string) { + cr, err := subresourcesCrclient.Create(context.TODO(), &unstructured.Unstructured{Object: map[string]interface{}{"apiVersion": "cr.bar.com/v1", "kind": "FooSub", "metadata": map[string]interface{}{"name": "test-2"}, "spec": map[string]interface{}{"replicas": 2}}}, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create cr: %v", err) + } + return cr, subresourcesCrdGVR.Group, "foosubs" + }, + subresource: "scale", + }, + { + name: "verify status subresource returns a table for replicationcontrollers", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1", + object: func(t *testing.T) (metav1.Object, string, string) { + rc := &v1.ReplicationController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "replicationcontroller-1", + }, + Spec: v1.ReplicationControllerSpec{ + Replicas: int32Ptr(2), + Selector: map[string]string{ + "label": "test-label", + }, + Template: &v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label": "test-label", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + {Name: "test-name", Image: "nonexistant-image"}, + }, + }, + }, + }, + } + rc, err := clientset.CoreV1().ReplicationControllers(testNamespace).Create(context.TODO(), rc, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create replicationcontroller: %v", err) + } + return rc, "", "replicationcontrollers" + }, + subresource: "status", + }, + { + name: "verify scale subresource returns a table for replicationcontrollers", + accept: "application/json;as=Table;g=meta.k8s.io;v=v1", + object: func(t *testing.T) (metav1.Object, string, string) { + rc := &v1.ReplicationController{ + ObjectMeta: metav1.ObjectMeta{ + Name: "replicationcontroller-2", + }, + Spec: v1.ReplicationControllerSpec{ + Replicas: int32Ptr(2), + Selector: map[string]string{ + "label": "test-label", + }, + Template: &v1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "label": "test-label", + }, + }, + Spec: v1.PodSpec{ + Containers: []v1.Container{ + {Name: "test-name", Image: "nonexistant-image"}, + }, + }, + }, + }, + } + rc, err := clientset.CoreV1().ReplicationControllers(testNamespace).Create(context.TODO(), rc, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("unable to create replicationcontroller: %v", err) + } + return rc, "", "replicationcontrollers" + }, + subresource: "scale", + }, + } + + for i := range testcases { + tc := testcases[i] + t.Run(tc.name, func(t *testing.T) { + obj, group, resource := tc.object(t) + + cfg := dynamic.ConfigFor(config) + if len(group) == 0 { + cfg = dynamic.ConfigFor(&restclient.Config{Host: s.URL}) + cfg.APIPath = "/api" + } else { + cfg.APIPath = "/apis" + } + cfg.GroupVersion = &schema.GroupVersion{Group: group, Version: "v1"} + + client, err := restclient.RESTClientFor(cfg) + if err != nil { + t.Fatal(err) + } + + res := client.Get(). + Resource(resource).NamespaceIfScoped(obj.GetNamespace(), len(obj.GetNamespace()) > 0). + SetHeader("Accept", tc.accept). + Name(obj.GetName()). + SubResource(tc.subresource). + Do(context.TODO()) + + resObj, err := res.Get() + if err != nil { + t.Fatalf("failed to retrieve object from response: %v", err) + } + actualKind := resObj.GetObjectKind().GroupVersionKind().Kind + if actualKind != "Table" { + t.Fatalf("Expected Kind 'Table', got '%v'", actualKind) + } + }) + } +} + func TestTransform(t *testing.T) { testNamespace := "test-transform" tearDown, config, _, err := fixtures.StartDefaultServer(t) @@ -2470,3 +2690,7 @@ func assertManagedFields(t *testing.T, obj *unstructured.Unstructured) { t.Errorf("unexpected empty managed fields in object: %v", obj) } } + +func int32Ptr(i int32) *int32 { + return &i +}