mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
Merge pull request #103516 from ykakarap/kubectl-subresources-apiserver
kubectl: apiserver changes to add --subresource support
This commit is contained in:
commit
9fbe66a486
@ -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"
|
||||
@ -582,6 +583,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.
|
||||
@ -2615,6 +2623,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)
|
||||
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -157,6 +157,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
|
||||
@ -315,6 +319,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))
|
||||
|
@ -153,6 +153,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
|
||||
@ -217,6 +221,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))
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -316,6 +316,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()
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -299,6 +299,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
|
||||
|
@ -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)))
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -878,7 +878,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()
|
||||
@ -889,6 +895,7 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
|
||||
Namer: meta.NewAccessor(),
|
||||
ClusterScoped: clusterScoped,
|
||||
}
|
||||
scaleScope.TableConvertor = scaleTable
|
||||
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) && subresources != nil && subresources.Scale != nil {
|
||||
scaleScope, err = scopeWithFieldManager(
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
@ -104,8 +105,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
|
||||
|
@ -1368,6 +1368,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)
|
||||
@ -2483,3 +2703,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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user