mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-23 11:50:44 +00:00
Use CRD validation field in server-side apply
This commit is contained in:
parent
7eeef1ac28
commit
c0617933d4
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
494
test/integration/apiserver/apply/apply_crd_test.go
Normal file
494
test/integration/apiserver/apply/apply_crd_test.go
Normal 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))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user