Merge pull request #63533 from sttts/sttts-required-with-status-subresource

Automatic merge from submit-queue (batch tested with PRs 59034, 63565, 63533). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

apiextensions: allow "required" at root with status subresource

In the subresources alpha we intentionally disallowed anything than `properties` at the root of the validation schema in order to allow us to project it to the .status subtree. By doing this we also disallowed `required` at the root which is necessary to enforce e.g. a spec to be set. This PR fixes this.

Moreover, it fixes that the restriction is only enforced when the status subresource is actually enabled. Before this PR we were enforcing the restriction as soon as the feature gate was enabled, leading to a backwards incompatible change.

```release-note
Allow "required" to be used at the CRD OpenAPI validation schema when the /status subresource is enabled.
```

There was an issue reporting the bug. But cannot find it.
This commit is contained in:
Kubernetes Submit Queue 2018-05-09 05:13:15 -07:00 committed by GitHub
commit ca76734126
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 96 additions and 15 deletions

View File

@ -27,8 +27,11 @@ go_test(
embed = [":go_default_library"], embed = [":go_default_library"],
deps = [ deps = [
"//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library", "//vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions:go_default_library",
"//vendor/k8s.io/apiextensions-apiserver/pkg/features:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library", "//vendor/k8s.io/apimachinery/pkg/util/validation/field:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library",
"//vendor/k8s.io/apiserver/pkg/util/feature/testing:go_default_library",
], ],
) )

View File

@ -106,7 +106,11 @@ func ValidateCustomResourceDefinitionSpec(spec *apiextensions.CustomResourceDefi
allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...) allErrs = append(allErrs, ValidateCustomResourceDefinitionNames(&spec.Names, fldPath.Child("names"))...)
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) { if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceValidation) {
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, fldPath.Child("validation"))...) statusEnabled := false
if spec.Subresources != nil && spec.Subresources.Status != nil {
statusEnabled = true
}
allErrs = append(allErrs, ValidateCustomResourceDefinitionValidation(spec.Validation, statusEnabled, fldPath.Child("validation"))...)
} else if spec.Validation != nil { } else if spec.Validation != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation")) allErrs = append(allErrs, field.Forbidden(fldPath.Child("validation"), "disabled by feature-gate CustomResourceValidation"))
} }
@ -187,7 +191,7 @@ type specStandardValidator interface {
} }
// ValidateCustomResourceDefinitionValidation statically validates // ValidateCustomResourceDefinitionValidation statically validates
func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, fldPath *field.Path) field.ErrorList { func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiextensions.CustomResourceValidation, statusSubresourceEnabled bool, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if customResourceValidation == nil { if customResourceValidation == nil {
@ -195,21 +199,19 @@ func ValidateCustomResourceDefinitionValidation(customResourceValidation *apiext
} }
if schema := customResourceValidation.OpenAPIV3Schema; schema != nil { if schema := customResourceValidation.OpenAPIV3Schema; schema != nil {
// if subresources are enabled, only properties is allowed inside the root schema // if subresources are enabled, only "properties" and "required" is allowed inside the root schema
if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) { if utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceSubresources) && statusSubresourceEnabled {
v := reflect.ValueOf(schema).Elem() v := reflect.ValueOf(schema).Elem()
fieldsPresent := 0
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
field := v.Field(i).Interface() // skip zero values
if !reflect.DeepEqual(field, reflect.Zero(reflect.TypeOf(field)).Interface()) { if value := v.Field(i).Interface(); reflect.DeepEqual(value, reflect.Zero(reflect.TypeOf(value)).Interface()) {
fieldsPresent++ continue
} }
}
if fieldsPresent > 1 || (fieldsPresent == 1 && v.FieldByName("Properties").IsNil()) { if name := v.Type().Field(i).Name; name != "Properties" && name != "Required" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf("if subresources for custom resources are enabled, only properties can be used at the root of the schema"))) allErrs = append(allErrs, field.Invalid(fldPath.Child("openAPIV3Schema"), *schema, fmt.Sprintf(`must only have "properties" or "required" at the root if the status subresource is enabled`)))
return allErrs break
}
} }
} }

View File

@ -19,9 +19,13 @@ package validation
import ( import (
"testing" "testing"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/validation/field"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilfeaturetesting "k8s.io/apiserver/pkg/util/feature/testing"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"k8s.io/apiextensions-apiserver/pkg/features"
) )
type validationMatch struct { type validationMatch struct {
@ -517,3 +521,70 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
} }
} }
} }
func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
defer utilfeaturetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.CustomResourceSubresources, true)()
tests := []struct {
name string
input apiextensions.CustomResourceValidation
statusEnabled bool
wantError bool
}{
{
name: "empty",
input: apiextensions.CustomResourceValidation{},
wantError: false,
},
{
name: "empty with status",
input: apiextensions.CustomResourceValidation{},
statusEnabled: true,
wantError: false,
},
{
name: "root type without status",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
},
},
statusEnabled: false,
wantError: false,
},
{
name: "root type with status",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
},
},
statusEnabled: true,
wantError: true,
},
{
name: "properties and required with status",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Properties: map[string]apiextensions.JSONSchemaProps{
"spec": {},
"status": {},
},
Required: []string{"spec", "status"},
},
},
statusEnabled: true,
wantError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ValidateCustomResourceDefinitionValidation(&tt.input, tt.statusEnabled, field.NewPath("spec", "validation"))
if !tt.wantError && len(got) > 0 {
t.Errorf("Expected no error, but got: %v", got)
} else if tt.wantError && len(got) == 0 {
t.Error("Expected error, but got none")
}
})
}
}

View File

@ -355,9 +355,12 @@ func TestValidationSchema(t *testing.T) {
// fields other than properties in root schema are not allowed // fields other than properties in root schema are not allowed
noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped) noxuDefinition := newNoxuValidationCRD(apiextensionsv1beta1.NamespaceScoped)
noxuDefinition.Spec.Subresources = &apiextensionsv1beta1.CustomResourceSubresources{
Status: &apiextensionsv1beta1.CustomResourceSubresourceStatus{},
}
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err == nil { if err == nil {
t.Fatalf("unexpected non-error: if subresources for custom resources are enabled, only properties can be used at the root of the schema") t.Fatalf(`unexpected non-error, expected: must only have "properties" or "required" at the root if the status subresource is enabled`)
} }
// make sure we are not restricting fields to properties even in subschemas // make sure we are not restricting fields to properties even in subschemas
@ -372,6 +375,7 @@ func TestValidationSchema(t *testing.T) {
}, },
}, },
}, },
Required: []string{"spec"},
} }
err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient) err = testserver.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil { if err != nil {

View File

@ -246,6 +246,7 @@ func checkForWatchCachePrimed(crd *apiextensionsv1beta1.CustomResourceDefinition
"gamma": "bar", "gamma": "bar",
"delta": "hello", "delta": "hello",
"epsilon": "foobar", "epsilon": "foobar",
"spec": map[string]interface{}{},
}, },
} }
if _, err := resourceClient.Create(instance); err != nil { if _, err := resourceClient.Create(instance); err != nil {