diff --git a/api/openapi-spec/swagger.json b/api/openapi-spec/swagger.json index e5f84db3381..95c9c24ccd2 100644 --- a/api/openapi-spec/swagger.json +++ b/api/openapi-spec/swagger.json @@ -16616,7 +16616,7 @@ "description": "CustomResourceSubresourceScale defines how to serve the scale subresource for CustomResources.", "properties": { "labelSelectorPath": { - "description": "LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. Only JSON paths without the array notation are allowed. Must be a JSON Path under .status. Must be set to work with HPA. If there is no value under the given path in the CustomResource, the status label selector value in the /scale subresource will default to the empty string.", + "description": "LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. Only JSON paths without the array notation are allowed. Must be a JSON Path under .status or .spec. Must be set to work with HPA. The field pointed by this JSON path must be a string field (not a complex selector struct) which contains a serialized label selector in string form. More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource If there is no value under the given path in the CustomResource, the status label selector value in the /scale subresource will default to the empty string.", "type": "string" }, "specReplicasPath": { diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go index 03344e2d025..8daa1f960ff 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/types.go @@ -393,8 +393,11 @@ type CustomResourceSubresourceScale struct { StatusReplicasPath string // LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. // Only JSON paths without the array notation are allowed. - // Must be a JSON Path under .status. + // Must be a JSON Path under .status or .spec. // Must be set to work with HPA. + // The field pointed by this JSON path must be a string field (not a complex selector struct) + // which contains a serialized label selector in string form. + // More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource // If there is no value under the given path in the CustomResource, the status label selector value in the /scale // subresource will default to the empty string. // +optional diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto index 166ad8bd135..7aa8c8b2042 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/generated.proto @@ -327,8 +327,11 @@ message CustomResourceSubresourceScale { // LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. // Only JSON paths without the array notation are allowed. - // Must be a JSON Path under .status. + // Must be a JSON Path under .status or .spec. // Must be set to work with HPA. + // The field pointed by this JSON path must be a string field (not a complex selector struct) + // which contains a serialized label selector in string form. + // More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource // If there is no value under the given path in the CustomResource, the status label selector value in the /scale // subresource will default to the empty string. // +optional diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go index 32d6ee7e8e1..715d107e97c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go @@ -412,8 +412,11 @@ type CustomResourceSubresourceScale struct { StatusReplicasPath string `json:"statusReplicasPath" protobuf:"bytes,2,opt,name=statusReplicasPath"` // LabelSelectorPath defines the JSON path inside of a CustomResource that corresponds to Scale.Status.Selector. // Only JSON paths without the array notation are allowed. - // Must be a JSON Path under .status. + // Must be a JSON Path under .status or .spec. // Must be set to work with HPA. + // The field pointed by this JSON path must be a string field (not a complex selector struct) + // which contains a serialized label selector in string form. + // More info: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions#scale-subresource // If there is no value under the given path in the CustomResource, the status label selector value in the /scale // subresource will default to the empty string. // +optional diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go index 59cbedaac83..1986930d6b1 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation.go @@ -794,8 +794,8 @@ func ValidateCustomResourceDefinitionSubresources(subresources *apiextensions.Cu if subresources.Scale.LabelSelectorPath != nil && len(*subresources.Scale.LabelSelectorPath) > 0 { if errs := validateSimpleJSONPath(*subresources.Scale.LabelSelectorPath, fldPath.Child("scale.labelSelectorPath")); len(errs) > 0 { allErrs = append(allErrs, errs...) - } else if !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".status.") { - allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.labelSelectorPath"), subresources.Scale.LabelSelectorPath, "should be a json path under .status")) + } else if !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".spec.") && !strings.HasPrefix(*subresources.Scale.LabelSelectorPath, ".status.") { + allErrs = append(allErrs, field.Invalid(fldPath.Child("scale.labelSelectorPath"), subresources.Scale.LabelSelectorPath, "should be a json path under either .spec or .status")) } } } diff --git a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go index 6cf96dcc575..14bc3be9f96 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go +++ b/staging/src/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/validation/validation_test.go @@ -1162,6 +1162,83 @@ func TestValidateCustomResourceDefinition(t *testing.T) { required("spec", "versions[1]", "schema", "openAPIV3Schema"), }, }, + { + name: "labelSelectorPath outside of .spec and .status", + resource: &apiextensions.CustomResourceDefinition{ + ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"}, + Spec: apiextensions.CustomResourceDefinitionSpec{ + Group: "group.com", + Version: "version0", + Versions: []apiextensions.CustomResourceDefinitionVersion{ + { + // null labelSelectorPath + Name: "version0", + Served: true, + Storage: true, + Subresources: &apiextensions.CustomResourceSubresources{ + Scale: &apiextensions.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + }, + }, + }, + { + // labelSelectorPath under .status + Name: "version1", + Served: true, + Storage: false, + Subresources: &apiextensions.CustomResourceSubresources{ + Scale: &apiextensions.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: strPtr(".status.labelSelector"), + }, + }, + }, + { + // labelSelectorPath under .spec + Name: "version2", + Served: true, + Storage: false, + Subresources: &apiextensions.CustomResourceSubresources{ + Scale: &apiextensions.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: strPtr(".spec.labelSelector"), + }, + }, + }, + { + // labelSelectorPath outside of .spec and .status + Name: "version3", + Served: true, + Storage: false, + Subresources: &apiextensions.CustomResourceSubresources{ + Scale: &apiextensions.CustomResourceSubresourceScale{ + SpecReplicasPath: ".spec.replicas", + StatusReplicasPath: ".status.replicas", + LabelSelectorPath: strPtr(".labelSelector"), + }, + }, + }, + }, + Scope: apiextensions.NamespaceScoped, + Names: apiextensions.CustomResourceDefinitionNames{ + Plural: "plural", + Singular: "singular", + Kind: "Plural", + ListKind: "PluralList", + }, + PreserveUnknownFields: pointer.BoolPtr(true), + }, + Status: apiextensions.CustomResourceDefinitionStatus{ + StoredVersions: []string{"version0"}, + }, + }, + errors: []validationMatch{ + invalid("spec", "versions[3]", "subresources", "scale", "labelSelectorPath"), + }, + }, } for _, tc := range tests {