Merge pull request #84713 from sttts/sttts-crd-defaulting-ga

Promote CRD defaulting to GA
This commit is contained in:
Kubernetes Prow Robot 2019-11-06 12:19:53 -08:00 committed by GitHub
commit 64be5ae5f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 134 additions and 510 deletions

View File

@ -593,7 +593,7 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
apiextensionsfeatures.CustomResourceSubresources: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
apiextensionsfeatures.CustomResourceWebhookConversion: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
apiextensionsfeatures.CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
apiextensionsfeatures.CustomResourceDefaulting: {Default: true, PreRelease: featuregate.Beta},
apiextensionsfeatures.CustomResourceDefaulting: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // TODO: remove in 1.18
// features that enable backwards compatibility but are scheduled to be removed
// ...

View File

@ -1048,9 +1048,6 @@ func allowDefaults(requestGV schema.GroupVersion, oldCRDSpec *apiextensions.Cust
// don't tighten validation on existing persisted data
return true
}
if !utilfeature.DefaultFeatureGate.Enabled(apiextensionsfeatures.CustomResourceDefaulting) {
return false
}
if requestGV == apiextensionsv1beta1.SchemeGroupVersion {
return false
}

View File

@ -1613,44 +1613,6 @@ func TestValidateCustomResourceDefinition(t *testing.T) {
invalid("spec", "versions[3]", "subresources", "scale", "labelSelectorPath"),
},
},
{
name: "defaults with disabled feature gate",
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: "plural.group.com"},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: singleVersionList,
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "number",
Default: jsonPtr(42.0),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(true),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
disabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
errors: []validationMatch{
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"), // disabled feature-gate
},
},
{
name: "defaults with enabled feature gate via v1beta1",
resource: &apiextensions.CustomResourceDefinition{
@ -5871,473 +5833,6 @@ func TestValidateCustomResourceDefinitionUpdate(t *testing.T) {
errors: []validationMatch{},
enabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
},
{
name: "ratcheting validation of defaults with disabled feature gate via v1beta1",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "number",
Default: jsonPtr(42.0),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "number",
Default: jsonPtr(42.0),
},
"b": {
Type: "number",
Default: jsonPtr(43.0),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
requestGV: apiextensionsv1beta1.SchemeGroupVersion,
errors: []validationMatch{},
disabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
},
{
name: "ratcheting validation of defaults with disabled feature gate via v1",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "number",
Default: jsonPtr(42.0),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "number",
Default: jsonPtr(42.0),
},
"b": {
Type: "number",
Default: jsonPtr(43.0),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
requestGV: apiextensionsv1.SchemeGroupVersion,
errors: []validationMatch{},
disabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
},
{
name: "ratcheting validation of defaults with disabled feature gate via v1, non-structural, no defaults before",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "number",
Default: jsonPtr(42.0),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
requestGV: apiextensionsv1.SchemeGroupVersion,
errors: []validationMatch{
forbidden("spec", "validation", "openAPIV3Schema", "properties[a]", "default"),
},
disabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
},
{
name: "ratcheting validation of defaults with disabled feature gate via v1, unpruned => unpruned",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {Type: "string"},
},
Default: jsonPtr(map[string]interface{}{
"unknown": "unknown",
}),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {Type: "string"},
},
Default: jsonPtr(map[string]interface{}{
"unknown": "unknown",
}),
},
"b": {
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {Type: "string"},
},
Default: jsonPtr(map[string]interface{}{
"unknown": "unknown",
}),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
requestGV: apiextensionsv1.SchemeGroupVersion,
errors: []validationMatch{},
disabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
},
{
name: "ratcheting validation of defaults with disabled feature gate via v1, pruned => unpruned",
old: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {Type: "string"},
},
Default: jsonPtr(map[string]interface{}{
"foo": "foo",
}),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
resource: &apiextensions.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{
Name: "plural.group.com",
ResourceVersion: "42",
},
Spec: apiextensions.CustomResourceDefinitionSpec{
Group: "group.com",
Version: "version",
Versions: []apiextensions.CustomResourceDefinitionVersion{
{
Name: "version",
Served: true,
Storage: true,
},
},
Scope: apiextensions.NamespaceScoped,
Names: apiextensions.CustomResourceDefinitionNames{
Plural: "plural",
Singular: "singular",
Kind: "Plural",
ListKind: "PluralList",
},
Validation: &apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"a": {
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {Type: "string"},
},
Default: jsonPtr(map[string]interface{}{
"foo": "foo",
}),
},
"b": {
Type: "object",
Properties: map[string]apiextensions.JSONSchemaProps{
"foo": {Type: "string"},
},
Default: jsonPtr(map[string]interface{}{
"unknown": "unknown",
}),
},
},
},
},
PreserveUnknownFields: pointer.BoolPtr(false),
},
Status: apiextensions.CustomResourceDefinitionStatus{
StoredVersions: []string{"version"},
},
},
requestGV: apiextensionsv1.SchemeGroupVersion,
errors: []validationMatch{
invalid("spec", "validation", "openAPIV3Schema", "properties[b]", "default"),
},
disabledFeatures: []featuregate.Feature{features.CustomResourceDefaulting},
},
{
name: "add default with enabled feature gate, structural schema, without pruning",
old: &apiextensions.CustomResourceDefinition{

View File

@ -62,8 +62,12 @@ const (
// owner: @sttts
// alpha: v1.15
// beta: v1.16
// GA: v1.17
//
// CustomResourceDefaulting enables OpenAPI defaulting in CustomResources.
//
// TODO: remove in 1.18
CustomResourceDefaulting featuregate.Feature = "CustomResourceDefaulting"
)
@ -79,5 +83,5 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS
CustomResourceSubresources: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
CustomResourceWebhookConversion: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
CustomResourcePublishOpenAPI: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
CustomResourceDefaulting: {Default: true, PreRelease: featuregate.Beta},
CustomResourceDefaulting: {Default: true, PreRelease: featuregate.GA, LockToDefault: true},
}

View File

@ -17,6 +17,9 @@ limitations under the License.
package apimachinery
import (
"fmt"
"time"
"github.com/onsi/ginkgo"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
@ -26,9 +29,12 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/diff"
"k8s.io/apimachinery/pkg/util/uuid"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/util/retry"
"k8s.io/kubernetes/test/e2e/framework"
@ -251,6 +257,128 @@ var _ = SIGDescribe("CustomResourceDefinition resources [Privileged:ClusterAdmin
}
})
/*
Release : v1.17
Testname: Custom Resource Definition, defaulting
Description: Create a custom resource definition without default. Create CR. Add default and read CR until
the default is applied. Create another CR. Remove default, add default for another field and read CR until
new field is defaulted, but old default stays.
*/
ginkgo.It("custom resource defaulting for requests and from storage works ", func() {
config, err := framework.LoadConfig()
framework.ExpectNoError(err, "loading config")
apiExtensionClient, err := clientset.NewForConfig(config)
framework.ExpectNoError(err, "initializing apiExtensionClient")
dynamicClient, err := dynamic.NewForConfig(config)
framework.ExpectNoError(err, "initializing dynamic client")
// Create CRD without default and waits for the resource to be recognized and available.
crd := fixtures.NewRandomNameV1CustomResourceDefinition(v1.ClusterScoped)
if crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties == nil {
crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties = map[string]v1.JSONSchemaProps{}
}
crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["a"] = v1.JSONSchemaProps{Type: "string"}
crd.Spec.Versions[0].Schema.OpenAPIV3Schema.Properties["b"] = v1.JSONSchemaProps{Type: "string"}
crd, err = fixtures.CreateNewV1CustomResourceDefinitionWatchUnsafe(crd, apiExtensionClient)
framework.ExpectNoError(err, "creating CustomResourceDefinition")
defer func() {
err = fixtures.DeleteV1CustomResourceDefinition(crd, apiExtensionClient)
framework.ExpectNoError(err, "deleting CustomResourceDefinition")
}()
// create CR without default in storage
name1 := names.SimpleNameGenerator.GenerateName("cr-1")
gvr := schema.GroupVersionResource{
Group: crd.Spec.Group,
Version: crd.Spec.Versions[0].Name,
Resource: crd.Spec.Names.Plural,
}
crClient := dynamicClient.Resource(gvr)
_, err = crClient.Create(&unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name1,
},
}}, metav1.CreateOptions{})
framework.ExpectNoError(err, "creating CR")
// Setting default for a to "A" and waiting for the CR to get defaulted on read
crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Patch(crd.Name, types.JSONPatchType, []byte(`[
{"op":"add","path":"/spec/versions/0/schema/openAPIV3Schema/properties/a/default", "value": "A"}
]`))
framework.ExpectNoError(err, "setting default for a to \"A\" in schema")
err = wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
u1, err := crClient.Get(name1, metav1.GetOptions{})
if err != nil {
return false, err
}
a, found, err := unstructured.NestedFieldNoCopy(u1.Object, "a")
if err != nil {
return false, err
}
if !found {
return false, nil
}
if a != "A" {
return false, fmt.Errorf("expected a:\"A\", but got a:%q", a)
}
return true, nil
})
framework.ExpectNoError(err, "waiting for CR to be defaulted on read")
// create CR with default in storage
name2 := names.SimpleNameGenerator.GenerateName("cr-2")
u2, err := crClient.Create(&unstructured.Unstructured{Object: map[string]interface{}{
"apiVersion": gvr.Group + "/" + gvr.Version,
"kind": crd.Spec.Names.Kind,
"metadata": map[string]interface{}{
"name": name2,
},
}}, metav1.CreateOptions{})
framework.ExpectNoError(err, "creating CR")
v, found, err := unstructured.NestedFieldNoCopy(u2.Object, "a")
framework.ExpectEqual(found, true, "\"a\" is defaulted")
framework.ExpectEqual(v, "A", "\"a\" is defaulted to \"A\"")
// Deleting default for a, adding default "B" for b and waiting for the CR to get defaulted on read for b
crd, err = apiExtensionClient.ApiextensionsV1().CustomResourceDefinitions().Patch(crd.Name, types.JSONPatchType, []byte(`[
{"op":"remove","path":"/spec/versions/0/schema/openAPIV3Schema/properties/a/default"},
{"op":"add","path":"/spec/versions/0/schema/openAPIV3Schema/properties/b/default", "value": "B"}
]`))
framework.ExpectNoError(err, "setting default for b to \"B\" and remove default for a")
err = wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (bool, error) {
u2, err := crClient.Get(name2, metav1.GetOptions{})
if err != nil {
return false, err
}
b, found, err := unstructured.NestedFieldNoCopy(u2.Object, "b")
if err != nil {
return false, err
}
if !found {
return false, nil
}
if b != "B" {
return false, fmt.Errorf("expected b:\"B\", but got b:%q", b)
}
a, found, err := unstructured.NestedFieldNoCopy(u2.Object, "a")
if err != nil {
return false, err
}
if !found {
return false, fmt.Errorf("expected a:\"A\" to be unchanged, but it was removed")
}
if a != "A" {
return false, fmt.Errorf("expected a:\"A\" to be unchanged, but it changed to %q", a)
}
return true, nil
})
framework.ExpectNoError(err, "waiting for CR to be defaulted on read for b and a staying the same")
})
})
func unstructuredToCRD(obj *unstructured.Unstructured) *v1.CustomResourceDefinition {