Use CRD validation field in server-side apply

This commit is contained in:
jennybuckley 2019-08-28 15:28:49 -07:00 committed by Jennifer Buckley
parent 7eeef1ac28
commit c0617933d4
23 changed files with 777 additions and 21 deletions

View File

@ -268,5 +268,21 @@ func (in *JSONSchemaProps) DeepCopy() *JSONSchemaProps {
}
}
if in.XListMapKeys != nil {
in, out := &in.XListMapKeys, &out.XListMapKeys
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.XListType != nil {
in, out := &in.XListType, &out.XListType
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return out
}

View File

@ -86,6 +86,29 @@ type JSONSchemaProps struct {
// - type: string
// - ... zero or more
XIntOrString bool
// x-kubernetes-list-map-keys annotates lists with the x-kubernetes-list-type `map` by specifying the keys used
// as the index of the map.
//
// This tag MUST only be used on lists that have the "x-kubernetes-list-type"
// extension set to "map". Also, the values specified for this attribute must
// be a scalar typed field of the child structure (no nesting is supported).
XListMapKeys []string
// x-kubernetes-list-type annotates a list to further describe its topology.
// This extension must only be used on lists and may have 3 possible values:
//
// 1) `atomic`: the list is treated as a single entity, like a scalar.
// Atomic lists will be entirely replaced when updated. This extension
// may be used on any type of list (struct, scalar, ...).
// 2) `set`:
// Sets are lists that must not have multiple times the same value. Each
// value must be a scalar (or another atomic type).
// 3) `map`:
// These lists are like maps in that their elements have a non-index key
// used to identify them. Order is preserved upon merge. The map tag
// must only be used on a list with struct elements.
XListType *string
}
// JSON represents any valid JSON value.

View File

@ -90,6 +90,29 @@ type JSONSchemaProps struct {
// - type: string
// - ... zero or more
XIntOrString bool `json:"x-kubernetes-int-or-string,omitempty" protobuf:"bytes,40,opt,name=xKubernetesIntOrString"`
// x-kubernetes-list-map-keys annotates lists with the x-kubernetes-list-type `map` by specifying the keys used
// as the index of the map.
//
// This tag MUST only be used on lists that have the "x-kubernetes-list-type"
// extension set to "map". Also, the values specified for this attribute must
// be a scalar typed field of the child structure (no nesting is supported).
XListMapKeys []string `json:"x-kubernetes-list-map-keys,omitempty" protobuf:"bytes,41,rep,name=xKubernetesListMapKeys"`
// x-kubernetes-list-type annotates a list to further describe its topology.
// This extension must only be used on lists and may have 3 possible values:
//
// 1) `atomic`: the list is treated as a single entity, like a scalar.
// Atomic lists will be entirely replaced when updated. This extension
// may be used on any type of list (struct, scalar, ...).
// 2) `set`:
// Sets are lists that must not have multiple times the same value. Each
// value must be a scalar (or another atomic type).
// 3) `map`:
// These lists are like maps in that their elements have a non-index key
// used to identify them. Order is preserved upon merge. The map tag
// must only be used on a list with struct elements.
XListType *string `json:"x-kubernetes-list-type,omitempty" protobuf:"bytes,42,opt,name=xKubernetesListType"`
}
// JSON represents any valid JSON value.

View File

@ -244,5 +244,21 @@ func (in *JSONSchemaProps) DeepCopy() *JSONSchemaProps {
}
}
if in.XListMapKeys != nil {
in, out := &in.XListMapKeys, &out.XListMapKeys
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.XListType != nil {
in, out := &in.XListType, &out.XListType
if *in == nil {
*out = nil
} else {
*out = new(string)
**out = **in
}
}
return out
}

View File

@ -90,6 +90,29 @@ type JSONSchemaProps struct {
// - type: string
// - ... zero or more
XIntOrString bool `json:"x-kubernetes-int-or-string,omitempty" protobuf:"bytes,40,opt,name=xKubernetesIntOrString"`
// x-kubernetes-list-map-keys annotates lists with the x-kubernetes-list-type `map` by specifying the keys used
// as the index of the map.
//
// This tag MUST only be used on lists that have the "x-kubernetes-list-type"
// extension set to "map". Also, the values specified for this attribute must
// be a scalar typed field of the child structure (no nesting is supported).
XListMapKeys []string `json:"x-kubernetes-list-map-keys,omitempty" protobuf:"bytes,41,rep,name=xKubernetesListMapKeys"`
// x-kubernetes-list-type annotates a list to further describe its topology.
// This extension must only be used on lists and may have 3 possible values:
//
// 1) `atomic`: the list is treated as a single entity, like a scalar.
// Atomic lists will be entirely replaced when updated. This extension
// may be used on any type of list (struct, scalar, ...).
// 2) `set`:
// Sets are lists that must not have multiple times the same value. Each
// value must be a scalar (or another atomic type).
// 3) `map`:
// These lists are like maps in that their elements have a non-index key
// used to identify them. Order is preserved upon merge. The map tag
// must only be used on a list with struct elements.
XListType *string `json:"x-kubernetes-list-type,omitempty" protobuf:"bytes,42,opt,name=xKubernetesListType"`
}
// JSON represents any valid JSON value.

View File

@ -788,6 +788,26 @@ func ValidateCustomResourceDefinitionOpenAPISchema(schema *apiextensions.JSONSch
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-preserve-unknown-fields"), *schema.XPreserveUnknownFields, "must be true or undefined"))
}
if schema.XListType != nil && schema.Type != "array" {
if len(schema.Type) == 0 {
allErrs = append(allErrs, field.Required(fldPath.Child("type"), "must be array if x-kubernetes-list-type is set"))
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("type"), schema.Type, "must be array if x-kubernetes-list-type is set"))
}
}
if schema.XListType != nil && *schema.XListType != "atomic" && *schema.XListType != "set" && *schema.XListType != "map" {
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-list-type"), *schema.XListType, "must be one of 'atomic', 'set', 'map', or unset"))
}
if len(schema.XListMapKeys) > 0 && (schema.XListType == nil || *schema.XListType != "map") {
if schema.XListType == nil {
allErrs = append(allErrs, field.Required(fldPath.Child("x-kubernetes-list-type"), "must be map if x-kubernetes-list-map-keys is non-empty"))
} else {
allErrs = append(allErrs, field.Invalid(fldPath.Child("x-kubernetes-list-type"), *schema.XListType, "must be map if x-kubernetes-list-map-keys is non-empty"))
}
}
return allErrs
}

View File

@ -6572,6 +6572,56 @@ func TestValidateCustomResourceDefinitionValidation(t *testing.T) {
opts: validationOptions{requireValidPropertyType: true, requireStructuralSchema: true},
wantError: true,
},
{
name: "invalid type with list type extension set",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "object",
XListType: strPtr("map"),
},
},
wantError: true,
},
{
name: "unset type with list type extension set",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
XListType: strPtr("map"),
},
},
wantError: true,
},
{
name: "invalid list type extension with list map keys extension set",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "array",
XListType: strPtr("set"),
XListMapKeys: []string{"key"},
},
},
wantError: true,
},
{
name: "unset list type extension with list map keys extension set",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "array",
XListMapKeys: []string{"key"},
},
},
wantError: true,
},
{
name: "invalid list type",
input: apiextensions.CustomResourceValidation{
OpenAPIV3Schema: &apiextensions.JSONSchemaProps{
Type: "array",
XListType: strPtr("invalid"),
},
},
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View File

@ -200,6 +200,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
s.GenericAPIServer.Authorizer,
c.GenericConfig.RequestTimeout,
time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
apiGroupInfo.StaticOpenAPISpec,
)
if err != nil {
return nil, err

View File

@ -40,6 +40,7 @@ import (
listers "k8s.io/apiextensions-apiserver/pkg/client/listers/apiextensions/internalversion"
"k8s.io/apiextensions-apiserver/pkg/controller/establish"
"k8s.io/apiextensions-apiserver/pkg/controller/finalizer"
"k8s.io/apiextensions-apiserver/pkg/controller/openapi/builder"
"k8s.io/apiextensions-apiserver/pkg/crdserverscheme"
apiextensionsfeatures "k8s.io/apiextensions-apiserver/pkg/features"
"k8s.io/apiextensions-apiserver/pkg/registry/customresource"
@ -74,11 +75,13 @@ import (
genericfilters "k8s.io/apiserver/pkg/server/filters"
"k8s.io/apiserver/pkg/storage/storagebackend"
utilfeature "k8s.io/apiserver/pkg/util/feature"
utilopenapi "k8s.io/apiserver/pkg/util/openapi"
"k8s.io/apiserver/pkg/util/webhook"
"k8s.io/client-go/scale"
"k8s.io/client-go/scale/scheme/autoscalingv1"
"k8s.io/client-go/tools/cache"
"k8s.io/klog"
"k8s.io/kube-openapi/pkg/util/proto"
)
// crdHandler serves the `/apis` endpoint.
@ -117,6 +120,11 @@ type crdHandler struct {
// minRequestTimeout applies to CR's list/watch calls
minRequestTimeout time.Duration
// staticOpenAPISpec is used as a base for the schema of CR's for the
// purpose of managing fields, it is how CR handlers get the structure
// of TypeMeta and ObjectMeta
staticOpenAPISpec *spec.Swagger
}
// crdInfo stores enough information to serve the storage for the custom resource
@ -160,7 +168,8 @@ func NewCustomResourceDefinitionHandler(
masterCount int,
authorizer authorizer.Authorizer,
requestTimeout time.Duration,
minRequestTimeout time.Duration) (*crdHandler, error) {
minRequestTimeout time.Duration,
staticOpenAPISpec *spec.Swagger) (*crdHandler, error) {
ret := &crdHandler{
versionDiscoveryHandler: versionDiscoveryHandler,
groupDiscoveryHandler: groupDiscoveryHandler,
@ -175,6 +184,7 @@ func NewCustomResourceDefinitionHandler(
authorizer: authorizer,
requestTimeout: requestTimeout,
minRequestTimeout: minRequestTimeout,
staticOpenAPISpec: staticOpenAPISpec,
}
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: ret.createCustomResourceDefinition,
@ -634,6 +644,25 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
structuralSchemas[v.Name] = s
}
var openAPIModels proto.Models
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) && r.staticOpenAPISpec != nil {
specs := []*spec.Swagger{}
for _, v := range crd.Spec.Versions {
s, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: false, StripDefaults: true, StripValueValidation: true})
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR schema")
}
specs = append(specs, s)
}
mergedOpenAPI := builder.MergeSpecs(r.staticOpenAPISpec, specs...)
openAPIModels, err = utilopenapi.ToProtoModels(mergedOpenAPI)
if err != nil {
utilruntime.HandleError(err)
return nil, fmt.Errorf("the server could not properly serve the CR schema")
}
}
for _, v := range crd.Spec.Versions {
safeConverter, unsafeConverter, err := r.converterFactory.NewConverter(crd)
if err != nil {
@ -799,12 +828,17 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
}
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
reqScope := *requestScopes[v.Name]
reqScope.FieldManager = fieldmanager.NewCRDFieldManager(
reqScope.FieldManager, err = fieldmanager.NewCRDFieldManager(
openAPIModels,
reqScope.Convertor,
reqScope.Defaulter,
reqScope.Kind.GroupVersion(),
reqScope.HubGroupVersion,
*crd.Spec.PreserveUnknownFields,
)
if err != nil {
return nil, err
}
requestScopes[v.Name] = &reqScope
}

View File

@ -244,6 +244,8 @@ func newExtensions(s *apiextensions.JSONSchemaProps) (*Extensions, error) {
ret := &Extensions{
XEmbeddedResource: s.XEmbeddedResource,
XIntOrString: s.XIntOrString,
XListMapKeys: s.XListMapKeys,
XListType: s.XListType,
}
if s.XPreserveUnknownFields != nil {

View File

@ -78,6 +78,12 @@ func (x *Extensions) toGoOpenAPI(ret *spec.Schema) {
if x.XIntOrString {
ret.VendorExtensible.AddExtension("x-kubernetes-int-or-string", true)
}
if len(x.XListMapKeys) > 0 {
ret.VendorExtensible.AddExtension("x-kubernetes-list-map-keys", x.XListMapKeys)
}
if x.XListType != nil {
ret.VendorExtensible.AddExtension("x-kubernetes-list-type", x.XListType)
}
}
func (v *ValueValidation) toGoOpenAPI(ret *spec.Schema) {

View File

@ -92,6 +92,29 @@ type Extensions struct {
// - type: string
// - ... zero or more
XIntOrString bool
// x-kubernetes-list-map-keys annotates lists with the x-kubernetes-list-type `map` by specifying the keys used
// as the index of the map.
//
// This tag MUST only be used on lists that have the "x-kubernetes-list-type"
// extension set to "map". Also, the values specified for this attribute must
// be a scalar typed field of the child structure (no nesting is supported).
XListMapKeys []string
// x-kubernetes-list-type annotates a list to further describe its topology.
// This extension must only be used on lists and may have 3 possible values:
//
// 1) `atomic`: the list is treated as a single entity, like a scalar.
// Atomic lists will be entirely replaced when updated. This extension
// may be used on any type of list (struct, scalar, ...).
// 2) `set`:
// Sets are lists that must not have multiple times the same value. Each
// value must be a scalar (or another atomic type).
// 3) `map`:
// These lists are like maps in that their elements have a non-index key
// used to identify them. Order is preserved upon merge. The map tag
// must only be used on a list with struct elements.
XListType *string
}
// +k8s:deepcopy-gen=true

View File

@ -305,6 +305,12 @@ func validateNestedValueValidation(v *NestedValueValidation, skipAnyOf, skipAllO
if v.ForbiddenExtensions.XIntOrString {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-int-or-string"), "must be false to be structural"))
}
if len(v.ForbiddenExtensions.XListMapKeys) > 0 {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-list-map-keys"), "must be empty to be structural"))
}
if v.ForbiddenExtensions.XListType != nil {
allErrs = append(allErrs, field.Forbidden(fldPath.Child("x-kubernetes-list-type"), "must be undefined to be structural"))
}
// forbid reasoning about metadata because it can lead to metadata restriction we don't want
if _, found := v.Properties["metadata"]; found {

View File

@ -21,8 +21,6 @@ import (
"testing"
fuzz "github.com/google/gofuzz"
"k8s.io/apimachinery/pkg/util/rand"
)
func TestValidateNestedValueValidationComplete(t *testing.T) {
@ -46,7 +44,6 @@ func TestValidateNestedValueValidationComplete(t *testing.T) {
for i := 0; i < tt.NumField(); i++ {
vv := &NestedValueValidation{}
x := reflect.ValueOf(&vv.ForbiddenGenerics).Elem()
i := rand.Intn(x.NumField())
fuzzer.Fuzz(x.Field(i).Addr().Interface())
errs := validateNestedValueValidation(vv, false, false, fieldLevel, nil)
@ -60,7 +57,6 @@ func TestValidateNestedValueValidationComplete(t *testing.T) {
for i := 0; i < tt.NumField(); i++ {
vv := &NestedValueValidation{}
x := reflect.ValueOf(&vv.ForbiddenExtensions).Elem()
i := rand.Intn(x.NumField())
fuzzer.Fuzz(x.Field(i).Addr().Interface())
errs := validateNestedValueValidation(vv, false, false, fieldLevel, nil)

View File

@ -246,6 +246,12 @@ func ConvertJSONSchemaPropsWithPostProcess(in *apiextensions.JSONSchemaProps, ou
if in.XEmbeddedResource {
out.VendorExtensible.AddExtension("x-kubernetes-embedded-resource", true)
}
if len(in.XListMapKeys) != 0 {
out.VendorExtensible.AddExtension("x-kubernetes-list-map-keys", in.XListMapKeys)
}
if in.XListType != nil {
out.VendorExtensible.AddExtension("x-kubernetes-list-type", in.XListType)
}
return nil
}

View File

@ -48,7 +48,7 @@ type FieldManager struct {
// NewFieldManager creates a new FieldManager that merges apply requests
// and update managed fields for other types of requests.
func NewFieldManager(models openapiproto.Models, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) (*FieldManager, error) {
typeConverter, err := internal.NewTypeConverter(models)
typeConverter, err := internal.NewTypeConverter(models, false)
if err != nil {
return nil, err
}
@ -66,19 +66,26 @@ func NewFieldManager(models openapiproto.Models, objectConverter runtime.ObjectC
}
// NewCRDFieldManager creates a new FieldManager specifically for
// CRDs. This doesn't use openapi models (and it doesn't support the
// validation field right now).
func NewCRDFieldManager(objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion) *FieldManager {
// CRDs. This allows for the possibility of fields which are not defined
// in models, as well as having no models defined at all.
func NewCRDFieldManager(models openapiproto.Models, objectConverter runtime.ObjectConvertor, objectDefaulter runtime.ObjectDefaulter, gv schema.GroupVersion, hub schema.GroupVersion, preserveUnknownFields bool) (_ *FieldManager, err error) {
var typeConverter internal.TypeConverter = internal.DeducedTypeConverter{}
if models != nil {
typeConverter, err = internal.NewTypeConverter(models, preserveUnknownFields)
if err != nil {
return nil, err
}
}
return &FieldManager{
typeConverter: internal.DeducedTypeConverter{},
typeConverter: typeConverter,
objectConverter: objectConverter,
objectDefaulter: objectDefaulter,
groupVersion: gv,
hubVersion: hub,
updater: merge.Updater{
Converter: internal.NewCRDVersionConverter(internal.DeducedTypeConverter{}, objectConverter, hub),
Converter: internal.NewCRDVersionConverter(typeConverter, objectConverter, hub),
},
}
}, nil
}
// Update is used when the object has already been merged (non-apply

View File

@ -58,12 +58,15 @@ func NewTestFieldManager() *fieldmanager.FieldManager {
Version: "v1",
}
return fieldmanager.NewCRDFieldManager(
f, _ := fieldmanager.NewCRDFieldManager(
nil,
&fakeObjectConvertor{},
&fakeObjectDefaulter{},
gv,
gv,
true,
)
return f
}
func TestFieldManagerCreation(t *testing.T) {

View File

@ -44,8 +44,8 @@ func (p *gvkParser) Type(gvk schema.GroupVersionKind) *typed.ParseableType {
return &t
}
func newGVKParser(models proto.Models) (*gvkParser, error) {
typeSchema, err := schemaconv.ToSchema(models)
func newGVKParser(models proto.Models, preserveUnknownFields bool) (*gvkParser, error) {
typeSchema, err := schemaconv.ToSchemaWithPreserveUnknownFields(models, preserveUnknownFields)
if err != nil {
return nil, fmt.Errorf("failed to convert models to schema: %v", err)
}

View File

@ -78,8 +78,8 @@ var _ TypeConverter = &typeConverter{}
// NewTypeConverter builds a TypeConverter from a proto.Models. This
// will automatically find the proper version of the object, and the
// corresponding schema information.
func NewTypeConverter(models proto.Models) (TypeConverter, error) {
parser, err := newGVKParser(models)
func NewTypeConverter(models proto.Models, preserveUnknownFields bool) (TypeConverter, error) {
parser, err := newGVKParser(models, preserveUnknownFields)
if err != nil {
return nil, err
}

View File

@ -48,7 +48,7 @@ func TestTypeConverter(t *testing.T) {
t.Fatalf("Failed to build OpenAPI models: %v", err)
}
tc, err := internal.NewTypeConverter(m)
tc, err := internal.NewTypeConverter(m, false)
if err != nil {
t.Fatalf("Failed to build TypeConverter: %v", err)
}
@ -214,7 +214,7 @@ spec:
b.Fatalf("Failed to build OpenAPI models: %v", err)
}
tc, err := internal.NewTypeConverter(m)
tc, err := internal.NewTypeConverter(m, false)
if err != nil {
b.Fatalf("Failed to build TypeConverter: %v", err)
}

View File

@ -39,7 +39,7 @@ func TestVersionConverter(t *testing.T) {
if err != nil {
t.Fatalf("Failed to build OpenAPI models: %v", err)
}
tc, err := internal.NewTypeConverter(m)
tc, err := internal.NewTypeConverter(m, false)
if err != nil {
t.Fatalf("Failed to build TypeConverter: %v", err)
}

View File

@ -77,6 +77,10 @@ type APIGroupInfo struct {
NegotiatedSerializer runtime.NegotiatedSerializer
// ParameterCodec performs conversions for query parameters passed to API calls
ParameterCodec runtime.ParameterCodec
// StaticOpenAPISpec is the spec derived from the definitions of all resources installed together.
// It is set during InstallAPIGroups, InstallAPIGroup, and InstallLegacyAPIGroup.
StaticOpenAPISpec *spec.Swagger
}
// GenericAPIServer contains state for a Kubernetes cluster api server.
@ -552,6 +556,9 @@ func (s *GenericAPIServer) getOpenAPIModels(apiPrefix string, apiGroupInfos ...*
if err != nil {
return nil, err
}
for _, apiGroupInfo := range apiGroupInfos {
apiGroupInfo.StaticOpenAPISpec = openAPISpec
}
return utilopenapi.ToProtoModels(openAPISpec)
}

View File

@ -0,0 +1,494 @@
/*
Copyright 2019 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apiserver
import (
"encoding/json"
"fmt"
"testing"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
genericfeatures "k8s.io/apiserver/pkg/features"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/dynamic"
featuregatetesting "k8s.io/component-base/featuregate/testing"
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
"k8s.io/kubernetes/test/integration/framework"
)
// TestApplyCRDNoSchema tests that CRDs and CRs can both be applied to with a PATCH request with the apply content type
// when there is no validation field provided.
func TestApplyCRDNoSchema(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
name := "mytest"
rest := apiExtensionClient.Discovery().RESTClient()
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: %s
spec:
replicas: 1`, apiVersion, kind, name))
result, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
}
// Patch object to change the number of replicas
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Body([]byte(`{"spec":{"replicas": 5}}`)).
DoRaw()
if err != nil {
t.Fatalf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result))
}
// Re-apply, we should get conflicts now, since the number of replicas was changed.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err == nil {
t.Fatalf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result)
}
status, ok := err.(*errors.StatusError)
if !ok {
t.Fatalf("Expecting to get conflicts as API error")
}
if len(status.Status().Details.Causes) < 1 {
t.Fatalf("Expecting to get at least one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes)
}
// Re-apply with force, should work fine.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("force", "true").
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result))
}
}
// TestApplyCRDStructuralSchema tests that when a CRD has a structural schema in its validation field,
// it will be used to construct the CR schema used by apply.
func TestApplyCRDStructuralSchema(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := fixtures.NewMultipleVersionNoxuCRD(apiextensionsv1beta1.ClusterScoped)
var c apiextensionsv1beta1.CustomResourceValidation
err = json.Unmarshal([]byte(`{
"openAPIV3Schema": {
"type": "object",
"properties": {
"spec": {
"type": "object",
"x-kubernetes-preserve-unknown-fields": true,
"properties": {
"cronSpec": {
"type": "string",
"pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$"
},
"ports": {
"type": "array",
"x-kubernetes-list-map-keys": [
"containerPort",
"protocol"
],
"x-kubernetes-list-type": "map",
"items": {
"properties": {
"containerPort": {
"format": "int32",
"type": "integer"
},
"hostIP": {
"type": "string"
},
"hostPort": {
"format": "int32",
"type": "integer"
},
"name": {
"type": "string"
},
"protocol": {
"type": "string"
}
},
"required": [
"containerPort"
],
"type": "object"
}
}
}
}
}
}
}`), &c)
if err != nil {
t.Fatal(err)
}
noxuDefinition.Spec.Validation = &c
falseBool := false
noxuDefinition.Spec.PreserveUnknownFields = &falseBool
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
name := "mytest"
rest := apiExtensionClient.Discovery().RESTClient()
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: %s
finalizers:
- test-finalizer
spec:
cronSpec: "* * * * */5"
replicas: 1
ports:
- name: x
containerPort: 80
protocol: TCP`, apiVersion, kind, name))
result, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
}
// Patch object to add another finalizer to the finalizers list
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Body([]byte(`{"metadata":{"finalizers":["another-one"]}}`)).
DoRaw()
if err != nil {
t.Fatalf("failed to add finalizer with merge patch: %v:\n%v", err, string(result))
}
// Re-apply the same config, should work fine, since finalizers should have the list-type extension 'set'.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to apply same config after adding a finalizer: %v:\n%v", err, string(result))
}
// Patch object to change the number of replicas
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Body([]byte(`{"spec":{"replicas": 5}}`)).
DoRaw()
if err != nil {
t.Fatalf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result))
}
// Re-apply, we should get conflicts now, since the number of replicas was changed.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err == nil {
t.Fatalf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result)
}
status, ok := err.(*errors.StatusError)
if !ok {
t.Fatalf("Expecting to get conflicts as API error")
}
if len(status.Status().Details.Causes) < 1 {
t.Fatalf("Expecting to get at least one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes)
}
// Re-apply with force, should work fine.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("force", "true").
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result))
}
// New applier tries to edit an existing list item, we should get conflicts.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test_2").
Body([]byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: %s
spec:
ports:
- name: "y"
containerPort: 80
protocol: TCP`, apiVersion, kind, name))).
DoRaw()
if err == nil {
t.Fatalf("Expecting to get conflicts when a different applier updates existing list item, got no error: %s", result)
}
status, ok = err.(*errors.StatusError)
if !ok {
t.Fatalf("Expecting to get conflicts as API error")
}
if len(status.Status().Details.Causes) < 1 {
t.Fatalf("Expecting to get at least one conflict when a different applier updates existing list item, got: %v", status.Status().Details.Causes)
}
// New applier tries to add a new list item, should work fine.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test_2").
Body([]byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: %s
spec:
ports:
- name: "y"
containerPort: 8080
protocol: TCP`, apiVersion, kind, name))).
DoRaw()
if err != nil {
t.Fatalf("failed to add a new list item to the object as a different applier: %v:\n%v", err, string(result))
}
}
// TestApplyCRDNonStructuralSchema tests that when a CRD has a non-structural schema in its validation field,
// it will be used to construct the CR schema used by apply, but any non-structural parts of the schema will be treated as
// nested maps (same as a CRD without a schema)
func TestApplyCRDNonStructuralSchema(t *testing.T) {
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
if err != nil {
t.Fatal(err)
}
defer server.TearDownFn()
config := server.ClientConfig
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
var c apiextensionsv1beta1.CustomResourceValidation
err = json.Unmarshal([]byte(`{
"openAPIV3Schema": {
"type": "object",
"properties": {
"spec": {
"anyOf": [
{
"type": "object",
"properties": {
"cronSpec": {
"type": "string",
"pattern": "^(\\d+|\\*)(/\\d+)?(\\s+(\\d+|\\*)(/\\d+)?){4}$"
}
}
}, {
"type": "string"
}
]
}
}
}
}`), &c)
if err != nil {
t.Fatal(err)
}
noxuDefinition.Spec.Validation = &c
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
name := "mytest"
rest := apiExtensionClient.Discovery().RESTClient()
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: %s
finalizers:
- test-finalizer
spec:
cronSpec: "* * * * */5"
replicas: 1`, apiVersion, kind, name))
result, err := rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
}
// Patch object to add another finalizer to the finalizers list
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Body([]byte(`{"metadata":{"finalizers":["another-one"]}}`)).
DoRaw()
if err != nil {
t.Fatalf("failed to add finalizer with merge patch: %v:\n%v", err, string(result))
}
// Re-apply the same config, should work fine, since finalizers should have the list-type extension 'set'.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to apply same config after adding a finalizer: %v:\n%v", err, string(result))
}
// Patch object to change the number of replicas
result, err = rest.Patch(types.MergePatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Body([]byte(`{"spec":{"replicas": 5}}`)).
DoRaw()
if err != nil {
t.Fatalf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result))
}
// Re-apply, we should get conflicts now, since the number of replicas was changed.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err == nil {
t.Fatalf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result)
}
status, ok := err.(*errors.StatusError)
if !ok {
t.Fatalf("Expecting to get conflicts as API error")
}
if len(status.Status().Details.Causes) < 1 {
t.Fatalf("Expecting to get at least one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes)
}
// Re-apply with force, should work fine.
result, err = rest.Patch(types.ApplyPatchType).
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Name(name).
Param("force", "true").
Param("fieldManager", "apply_test").
Body(yamlBody).
DoRaw()
if err != nil {
t.Fatalf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result))
}
}