diff --git a/test/integration/apiserver/unstructured_test.go b/test/integration/apiserver/unstructured_test.go new file mode 100644 index 00000000000..167b1c09b1b --- /dev/null +++ b/test/integration/apiserver/unstructured_test.go @@ -0,0 +1,108 @@ +/* +Copyright 2024 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 ( + "context" + "encoding/json" + "errors" + "fmt" + "testing" + + "golang.org/x/net/http2" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1" + clientset "k8s.io/client-go/kubernetes" + kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/framework" +) + +// TestRequestObjectConvertibleToUnstructured tests that write requests fail if the request object +// is not convertible to unstructured. The ability to convert an object to unstructured ensures that +// it can be encoded as JSON and that field managers can be determined. +func TestRequestObjectConvertibleToUnstructured(t *testing.T) { + server := kubeapiservertesting.StartTestServerOrDie(t, nil, []string{}, framework.SharedEtcd()) + defer server.TearDownFn() + + for i, raw := range []string{ + ``, + `"`, + `{`, + `[`, + `1z`, + `z`, + } { + // The Protobuf request encoding is required. Invalid JSON cannot be embedded in a + // JSON object or array without making the containing object or array also invalid. + protoConfig := server.ClientConfig + protoConfig.ContentConfig.ContentType = runtime.ContentTypeProtobuf + protoConfig.ContentConfig.AcceptContentTypes = runtime.ContentTypeProtobuf + protoClient, err := clientset.NewForConfig(protoConfig) + if err != nil { + t.Fatalf("unexpected error creating proto client: %v", err) + } + + createError := new(http2.StreamError) + if _, err := protoClient.AppsV1().ControllerRevisions("default").Create(context.TODO(), &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test-revision-create-%d", i), + }, + Data: runtime.RawExtension{Raw: []byte(raw)}, + }, metav1.CreateOptions{}); errors.As(err, createError) && createError.Code == http2.ErrCodeInternal { + t.Logf("create returned internal error as expected with rawextension %#v: %v", raw, err) + } else { + t.Errorf("create returned unexpected error: %#v", err) + } + + var marshalerError *json.MarshalerError + if _, err := protoClient.AppsV1().ControllerRevisions("default").Apply(context.TODO(), appsv1applyconfigurations.ControllerRevision("test-revision-apply", "default"). + WithData(runtime.RawExtension{Raw: []byte(raw)}), + metav1.ApplyOptions{}); errors.As(err, &marshalerError) { + // In this case the error is currently client-side, since apply request + // bodies must be encoded as JSON. Included here to cover the future + // possibility of Protobuf-encoded apply configurations. + t.Logf("apply returned client-side marshaler error as expected with rawextension %#v: %v", raw, err) + } else { + t.Errorf("apply returned unexpected error: %#v", err) + } + + // Create an object to be updated. If the object does not exist, then the update + // will short-circuit on "not found" before it encounters the error that is + // interesting to this test. + existing, err := protoClient.AppsV1().ControllerRevisions("default").Create(context.TODO(), &appsv1.ControllerRevision{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test-revision-update-%d", i), + }, + Data: runtime.RawExtension{Raw: []byte(`{}`)}, + }, metav1.CreateOptions{}) + if err != nil { + t.Errorf("expected nil create error, got: %v", err) + continue + } + + updateError := new(http2.StreamError) + existing.Data = runtime.RawExtension{Raw: []byte(raw)} + if _, err := protoClient.AppsV1().ControllerRevisions(existing.Namespace).Update(context.TODO(), existing, metav1.UpdateOptions{}); errors.As(err, updateError) && updateError.Code == http2.ErrCodeInternal { + t.Logf("update returned internal error as expected with rawextension %#v: %v", raw, err) + } else { + t.Errorf("update returned unexpected error: %#v", err) + } + } +}