mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-28 04:53:11 +00:00
apiextensions: allow "required" at root with status subresource
This commit is contained in:
parent
e6b6e5c4b4
commit
1558da15f1
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -245,6 +245,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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user