Skip to content

Commit e682310

Browse files
authored
Merge pull request kubernetes#82438 from liggitt/apply-fallback
Fallback to schemaless apply behavior for custom resources with unhandled schemas
2 parents ab73a01 + 3904e14 commit e682310

File tree

2 files changed

+147
-21
lines changed

2 files changed

+147
-21
lines changed

staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/customresource_handler.go

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -644,27 +644,10 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
644644
structuralSchemas[v.Name] = s
645645
}
646646

647-
var openAPIModels proto.Models
648-
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) && r.staticOpenAPISpec != nil {
649-
specs := []*spec.Swagger{}
650-
for _, v := range crd.Spec.Versions {
651-
s, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: false, StripDefaults: true, StripValueValidation: true})
652-
if err != nil {
653-
utilruntime.HandleError(err)
654-
return nil, fmt.Errorf("the server could not properly serve the CR schema")
655-
}
656-
specs = append(specs, s)
657-
}
658-
mergedOpenAPI, err := builder.MergeSpecs(r.staticOpenAPISpec, specs...)
659-
if err != nil {
660-
utilruntime.HandleError(err)
661-
return nil, fmt.Errorf("the server could not properly merge the CR schema")
662-
}
663-
openAPIModels, err = utilopenapi.ToProtoModels(mergedOpenAPI)
664-
if err != nil {
665-
utilruntime.HandleError(err)
666-
return nil, fmt.Errorf("the server could not properly serve the CR schema")
667-
}
647+
openAPIModels, err := buildOpenAPIModelsForApply(r.staticOpenAPISpec, crd)
648+
if err != nil {
649+
utilruntime.HandleError(fmt.Errorf("error building openapi models for %s: %v", crd.Name, err))
650+
openAPIModels = nil
668651
}
669652

670653
for _, v := range crd.Spec.Versions {
@@ -1239,3 +1222,35 @@ func serverStartingError() error {
12391222
}
12401223
return err
12411224
}
1225+
1226+
// buildOpenAPIModelsForApply constructs openapi models from any validation schemas specified in the custom resource,
1227+
// and merges it with the models defined in the static OpenAPI spec.
1228+
// Returns nil models if the ServerSideApply feature is disabled, or the static spec is nil, or an error is encountered.
1229+
func buildOpenAPIModelsForApply(staticOpenAPISpec *spec.Swagger, crd *apiextensions.CustomResourceDefinition) (proto.Models, error) {
1230+
if !utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
1231+
return nil, nil
1232+
}
1233+
if staticOpenAPISpec == nil {
1234+
return nil, nil
1235+
}
1236+
1237+
specs := []*spec.Swagger{}
1238+
for _, v := range crd.Spec.Versions {
1239+
s, err := builder.BuildSwagger(crd, v.Name, builder.Options{V2: false, StripDefaults: true, StripValueValidation: true})
1240+
if err != nil {
1241+
return nil, err
1242+
}
1243+
specs = append(specs, s)
1244+
}
1245+
1246+
mergedOpenAPI, err := builder.MergeSpecs(staticOpenAPISpec, specs...)
1247+
if err != nil {
1248+
return nil, err
1249+
}
1250+
1251+
models, err := utilopenapi.ToProtoModels(mergedOpenAPI)
1252+
if err != nil {
1253+
return nil, err
1254+
}
1255+
return models, nil
1256+
}

test/integration/apiserver/apply/apply_crd_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -607,3 +607,114 @@ func verifyNumPorts(t *testing.T, b []byte, n int) {
607607
t.Fatalf("expected %v ports but got %v:\n%v", expected, actual, string(b))
608608
}
609609
}
610+
611+
// TestApplyCRDUnhandledSchema tests that when a CRD has a schema that kube-openapi ToProtoModels cannot handle correctly,
612+
// apply falls back to non-schema behavior
613+
func TestApplyCRDUnhandledSchema(t *testing.T) {
614+
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, genericfeatures.ServerSideApply, true)()
615+
616+
server, err := apiservertesting.StartTestServer(t, apiservertesting.NewDefaultTestServerOptions(), nil, framework.SharedEtcd())
617+
if err != nil {
618+
t.Fatal(err)
619+
}
620+
defer server.TearDownFn()
621+
config := server.ClientConfig
622+
623+
apiExtensionClient, err := clientset.NewForConfig(config)
624+
if err != nil {
625+
t.Fatal(err)
626+
}
627+
dynamicClient, err := dynamic.NewForConfig(config)
628+
if err != nil {
629+
t.Fatal(err)
630+
}
631+
632+
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
633+
634+
// This is a schema that kube-openapi ToProtoModels does not handle correctly.
635+
// https://github.com/kubernetes/kubernetes/blob/38752f7f99869ed65fb44378360a517649dc2f83/vendor/k8s.io/kube-openapi/pkg/util/proto/document.go#L184
636+
var c apiextensionsv1beta1.CustomResourceValidation
637+
err = json.Unmarshal([]byte(`{
638+
"openAPIV3Schema": {
639+
"properties": {
640+
"TypeFooBar": {
641+
"type": "array"
642+
}
643+
}
644+
}
645+
}`), &c)
646+
if err != nil {
647+
t.Fatal(err)
648+
}
649+
noxuDefinition.Spec.Validation = &c
650+
651+
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
652+
if err != nil {
653+
t.Fatal(err)
654+
}
655+
656+
kind := noxuDefinition.Spec.Names.Kind
657+
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
658+
name := "mytest"
659+
660+
rest := apiExtensionClient.Discovery().RESTClient()
661+
yamlBody := []byte(fmt.Sprintf(`
662+
apiVersion: %s
663+
kind: %s
664+
metadata:
665+
name: %s
666+
spec:
667+
replicas: 1`, apiVersion, kind, name))
668+
result, err := rest.Patch(types.ApplyPatchType).
669+
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
670+
Name(name).
671+
Param("fieldManager", "apply_test").
672+
Body(yamlBody).
673+
DoRaw()
674+
if err != nil {
675+
t.Fatalf("failed to create custom resource with apply: %v:\n%v", err, string(result))
676+
}
677+
verifyReplicas(t, result, 1)
678+
679+
// Patch object to change the number of replicas
680+
result, err = rest.Patch(types.MergePatchType).
681+
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
682+
Name(name).
683+
Body([]byte(`{"spec":{"replicas": 5}}`)).
684+
DoRaw()
685+
if err != nil {
686+
t.Fatalf("failed to update number of replicas with merge patch: %v:\n%v", err, string(result))
687+
}
688+
verifyReplicas(t, result, 5)
689+
690+
// Re-apply, we should get conflicts now, since the number of replicas was changed.
691+
result, err = rest.Patch(types.ApplyPatchType).
692+
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
693+
Name(name).
694+
Param("fieldManager", "apply_test").
695+
Body(yamlBody).
696+
DoRaw()
697+
if err == nil {
698+
t.Fatalf("Expecting to get conflicts when applying object after updating replicas, got no error: %s", result)
699+
}
700+
status, ok := err.(*errors.StatusError)
701+
if !ok {
702+
t.Fatalf("Expecting to get conflicts as API error")
703+
}
704+
if len(status.Status().Details.Causes) != 1 {
705+
t.Fatalf("Expecting to get one conflict when applying object after updating replicas, got: %v", status.Status().Details.Causes)
706+
}
707+
708+
// Re-apply with force, should work fine.
709+
result, err = rest.Patch(types.ApplyPatchType).
710+
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
711+
Name(name).
712+
Param("force", "true").
713+
Param("fieldManager", "apply_test").
714+
Body(yamlBody).
715+
DoRaw()
716+
if err != nil {
717+
t.Fatalf("failed to apply object with force after updating replicas: %v:\n%v", err, string(result))
718+
}
719+
verifyReplicas(t, result, 1)
720+
}

0 commit comments

Comments
 (0)