Merge pull request #103516 from ykakarap/kubectl-subresources-apiserver
kubectl: apiserver changes to add --subresource support
This commit is contained in:
		| @@ -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 | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Kubernetes Prow Robot
					Kubernetes Prow Robot