Skip to content

Commit 7243fa6

Browse files
authored
Merge pull request kubernetes#128641 from benluddy/e2e-cbor-client-compat
KEP-4222: Fix JSON fallback for clients using default content-type and add E2E client test.
2 parents 631d83b + 42d3e97 commit 7243fa6

File tree

5 files changed

+156
-56
lines changed

5 files changed

+156
-56
lines changed

staging/src/k8s.io/client-go/rest/client.go

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ func (c *RESTClient) Delete() *Request {
238238

239239
// APIVersion returns the APIVersion this RESTClient is expected to use.
240240
func (c *RESTClient) APIVersion() schema.GroupVersion {
241-
return c.content.GetClientContentConfig().GroupVersion
241+
config, _ := c.content.GetClientContentConfig()
242+
return config.GroupVersion
242243
}
243244

244245
// requestClientContentConfigProvider observes HTTP 415 (Unsupported Media Type) responses to detect
@@ -257,25 +258,35 @@ type requestClientContentConfigProvider struct {
257258
}
258259

259260
// GetClientContentConfig returns the ClientContentConfig that should be used for new requests by
260-
// this client.
261-
func (p *requestClientContentConfigProvider) GetClientContentConfig() ClientContentConfig {
261+
// this client and true if the request ContentType was selected by default.
262+
func (p *requestClientContentConfigProvider) GetClientContentConfig() (ClientContentConfig, bool) {
263+
config := p.base
264+
265+
defaulted := config.ContentType == ""
266+
if defaulted {
267+
config.ContentType = "application/json"
268+
}
269+
262270
if !clientfeatures.FeatureGates().Enabled(clientfeatures.ClientsAllowCBOR) {
263-
return p.base
271+
return config, defaulted
272+
}
273+
274+
if defaulted && clientfeatures.FeatureGates().Enabled(clientfeatures.ClientsPreferCBOR) {
275+
config.ContentType = "application/cbor"
264276
}
265277

266278
if sawUnsupportedMediaTypeForCBOR := p.sawUnsupportedMediaTypeForCBOR.Load(); !sawUnsupportedMediaTypeForCBOR {
267-
return p.base
279+
return config, defaulted
268280
}
269281

270-
if mediaType, _, _ := mime.ParseMediaType(p.base.ContentType); mediaType != runtime.ContentTypeCBOR {
271-
return p.base
282+
if mediaType, _, _ := mime.ParseMediaType(config.ContentType); mediaType != runtime.ContentTypeCBOR {
283+
return config, defaulted
272284
}
273285

274-
config := p.base
275-
// The default ClientContentConfig sets ContentType to CBOR and the client has previously
276-
// received an HTTP 415 in response to a CBOR request. Override ContentType to JSON.
286+
// The effective ContentType is CBOR and the client has previously received an HTTP 415 in
287+
// response to a CBOR request. Override ContentType to JSON.
277288
config.ContentType = runtime.ContentTypeJSON
278-
return config
289+
return config, defaulted
279290
}
280291

281292
// UnsupportedMediaType reports that the server has responded to a request with HTTP 415 Unsupported

staging/src/k8s.io/client-go/rest/request.go

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -156,14 +156,10 @@ func NewRequest(c *RESTClient) *Request {
156156
timeout = c.Client.Timeout
157157
}
158158

159-
contentConfig := c.content.GetClientContentConfig()
160-
contentTypeNotSet := len(contentConfig.ContentType) == 0
161-
if contentTypeNotSet {
162-
contentConfig.ContentType = "application/json"
163-
if clientfeatures.FeatureGates().Enabled(clientfeatures.ClientsAllowCBOR) && clientfeatures.FeatureGates().Enabled(clientfeatures.ClientsPreferCBOR) {
164-
contentConfig.ContentType = "application/cbor"
165-
}
166-
}
159+
// A request needs to know whether the content type was explicitly configured or selected by
160+
// default in order to support the per-request Protobuf override used by clients generated
161+
// with --prefers-protobuf.
162+
contentConfig, contentTypeDefaulted := c.content.GetClientContentConfig()
167163

168164
r := &Request{
169165
c: c,
@@ -176,7 +172,7 @@ func NewRequest(c *RESTClient) *Request {
176172
warningHandler: c.warningHandler,
177173

178174
contentConfig: contentConfig,
179-
contentTypeNotSet: contentTypeNotSet,
175+
contentTypeNotSet: contentTypeDefaulted,
180176
}
181177

182178
r.setAcceptHeader()

test/e2e/apimachinery/protocol.go

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,21 @@ import (
2626
v1 "k8s.io/api/core/v1"
2727
apierrors "k8s.io/apimachinery/pkg/api/errors"
2828
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2930
"k8s.io/apimachinery/pkg/fields"
31+
"k8s.io/apimachinery/pkg/runtime"
3032
"k8s.io/apimachinery/pkg/watch"
33+
"k8s.io/client-go/dynamic"
34+
clientfeatures "k8s.io/client-go/features"
35+
clientfeaturestesting "k8s.io/client-go/features/testing"
3136
"k8s.io/client-go/kubernetes"
32-
admissionapi "k8s.io/pod-security-admission/api"
33-
37+
aggregatorclientset "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset"
38+
"k8s.io/kubernetes/test/e2e/feature"
3439
"k8s.io/kubernetes/test/e2e/framework"
40+
imageutils "k8s.io/kubernetes/test/utils/image"
41+
admissionapi "k8s.io/pod-security-admission/api"
42+
samplev1alpha1 "k8s.io/sample-apiserver/pkg/apis/wardle/v1alpha1"
43+
samplev1alpha1client "k8s.io/sample-apiserver/pkg/generated/clientset/versioned/typed/wardle/v1alpha1"
3544
)
3645

3746
var _ = SIGDescribe("client-go should negotiate", func() {
@@ -96,3 +105,67 @@ var _ = SIGDescribe("client-go should negotiate", func() {
96105
})
97106
}
98107
})
108+
109+
var _ = SIGDescribe("CBOR", feature.CBOR, func() {
110+
f := framework.NewDefaultFramework("cbor")
111+
f.NamespacePodSecurityLevel = admissionapi.LevelBaseline
112+
113+
// Must be serial to avoid conflict with other tests that set up a sample apiserver.
114+
f.It("clients remain compatible with the 1.17 sample-apiserver", f.WithSerial(), func(ctx context.Context) {
115+
clientfeaturestesting.SetFeatureDuringTest(g.GinkgoTB(), clientfeatures.ClientsAllowCBOR, true)
116+
clientfeaturestesting.SetFeatureDuringTest(g.GinkgoTB(), clientfeatures.ClientsPreferCBOR, true)
117+
118+
clientConfig, err := framework.LoadConfig()
119+
framework.ExpectNoError(err)
120+
121+
aggregatorClient, err := aggregatorclientset.NewForConfig(clientConfig)
122+
framework.ExpectNoError(err)
123+
124+
dynamicClient, err := dynamic.NewForConfig(clientConfig)
125+
framework.ExpectNoError(err)
126+
127+
objectNames := generateSampleAPIServerObjectNames(f.Namespace.Name)
128+
g.DeferCleanup(func(ctx context.Context) {
129+
cleanupSampleAPIServer(ctx, f.ClientSet, aggregatorClient, objectNames, samplev1alpha1.SchemeGroupVersion.Version+"."+samplev1alpha1.SchemeGroupVersion.Group)
130+
})
131+
SetUpSampleAPIServer(ctx, f, aggregatorClient, imageutils.GetE2EImage(imageutils.APIServer), objectNames, samplev1alpha1.SchemeGroupVersion.Group, samplev1alpha1.SchemeGroupVersion.Version)
132+
133+
flunder := samplev1alpha1.Flunder{
134+
ObjectMeta: metav1.ObjectMeta{
135+
Name: "test-flunder",
136+
},
137+
}
138+
139+
g.By("making requests with a generated client", func() {
140+
sampleClient, err := samplev1alpha1client.NewForConfig(clientConfig)
141+
framework.ExpectNoError(err)
142+
143+
_, err = sampleClient.Flunders(f.Namespace.Name).List(ctx, metav1.ListOptions{LabelSelector: "a,!a"})
144+
framework.ExpectNoError(err, "Failed to list with generated client")
145+
146+
_, err = sampleClient.Flunders(f.Namespace.Name).Create(ctx, &flunder, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
147+
o.Expect(err).To(o.MatchError(apierrors.IsUnsupportedMediaType, "Expected 415 (Unsupported Media Type) response on first write with generated client"))
148+
149+
_, err = sampleClient.Flunders(f.Namespace.Name).Create(ctx, &flunder, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
150+
framework.ExpectNoError(err, "Expected subsequent writes to succeed with generated client")
151+
})
152+
153+
g.By("making requests with a dynamic client", func() {
154+
unstructuredFlunderContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&flunder)
155+
framework.ExpectNoError(err)
156+
unstructuredFlunder := &unstructured.Unstructured{Object: unstructuredFlunderContent}
157+
158+
flunderDynamicClient := dynamicClient.Resource(samplev1alpha1.SchemeGroupVersion.WithResource("flunders")).Namespace(f.Namespace.Name)
159+
160+
list, err := flunderDynamicClient.List(ctx, metav1.ListOptions{LabelSelector: "a,!a"})
161+
framework.ExpectNoError(err, "Failed to list with dynamic client")
162+
o.Expect(list.GetObjectKind().GroupVersionKind()).To(o.Equal(samplev1alpha1.SchemeGroupVersion.WithKind("FlunderList")))
163+
164+
_, err = flunderDynamicClient.Create(ctx, unstructuredFlunder, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
165+
o.Expect(err).To(o.MatchError(apierrors.IsUnsupportedMediaType, "Expected 415 (Unsupported Media Type) response on first write with dynamic client"))
166+
167+
_, err = flunderDynamicClient.Create(ctx, unstructuredFlunder, metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}})
168+
framework.ExpectNoError(err, "Expected subsequent writes to succeed with dynamic client")
169+
})
170+
})
171+
})

test/e2e/feature/feature.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ var (
3434
// TODO: document the feature (owning SIG, when to use this feature for a test)
3535
BoundServiceAccountTokenVolume = framework.WithFeature(framework.ValidFeatures.Add("BoundServiceAccountTokenVolume"))
3636

37+
// Owner: sig-api-machinery
38+
// Marks tests that exercise the CBOR data format for serving or storage.
39+
CBOR = framework.WithFeature(framework.ValidFeatures.Add("CBOR"))
40+
3741
// TODO: document the feature (owning SIG, when to use this feature for a test)
3842
CloudProvider = framework.WithFeature(framework.ValidFeatures.Add("CloudProvider"))
3943

test/integration/client/client_test.go

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,44 +2037,60 @@ func TestUnsupportedMediaTypeCircuitBreaker(t *testing.T) {
20372037
server := kubeapiservertesting.StartTestServerOrDie(t, nil, framework.DefaultTestServerFlags(), framework.SharedEtcd())
20382038
t.Cleanup(server.TearDownFn)
20392039

2040-
config := rest.CopyConfig(server.ClientConfig)
2041-
config.ContentType = "application/cbor"
2042-
config.AcceptContentTypes = "application/json"
2040+
for _, tc := range []struct {
2041+
name string
2042+
contentType string
2043+
}{
2044+
{
2045+
name: "default content type",
2046+
contentType: "",
2047+
},
2048+
{
2049+
name: "explicit content type",
2050+
contentType: "application/cbor",
2051+
},
2052+
} {
2053+
t.Run(tc.name, func(t *testing.T) {
2054+
config := rest.CopyConfig(server.ClientConfig)
2055+
config.ContentType = tc.contentType
2056+
config.AcceptContentTypes = "application/json"
20432057

2044-
client, err := corev1client.NewForConfig(config)
2045-
if err != nil {
2046-
t.Fatal(err)
2047-
}
2058+
client, err := corev1client.NewForConfig(config)
2059+
if err != nil {
2060+
t.Fatal(err)
2061+
}
20482062

2049-
if _, err := client.Namespaces().Create(
2050-
context.TODO(),
2051-
&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-client-415"}},
2052-
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
2053-
); !apierrors.IsUnsupportedMediaType(err) {
2054-
t.Errorf("expected to receive unsupported media type on first cbor request, got: %v", err)
2055-
}
2063+
if _, err := client.Namespaces().Create(
2064+
context.TODO(),
2065+
&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-client-415"}},
2066+
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
2067+
); !apierrors.IsUnsupportedMediaType(err) {
2068+
t.Errorf("expected to receive unsupported media type on first cbor request, got: %v", err)
2069+
}
20562070

2057-
// Requests from this client should fall back from application/cbor to application/json.
2058-
if _, err := client.Namespaces().Create(
2059-
context.TODO(),
2060-
&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-client-415"}},
2061-
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
2062-
); err != nil {
2063-
t.Errorf("expected to receive nil error on subsequent cbor request, got: %v", err)
2064-
}
2071+
// Requests from this client should fall back from application/cbor to application/json.
2072+
if _, err := client.Namespaces().Create(
2073+
context.TODO(),
2074+
&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-client-415"}},
2075+
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
2076+
); err != nil {
2077+
t.Errorf("expected to receive nil error on subsequent cbor request, got: %v", err)
2078+
}
20652079

2066-
// The circuit breaker trips on a per-client basis, so it should not begin tripped for a
2067-
// fresh client with identical config.
2068-
client, err = corev1client.NewForConfig(config)
2069-
if err != nil {
2070-
t.Fatal(err)
2071-
}
2080+
// The circuit breaker trips on a per-client basis, so it should not begin tripped for a
2081+
// fresh client with identical config.
2082+
client, err = corev1client.NewForConfig(config)
2083+
if err != nil {
2084+
t.Fatal(err)
2085+
}
20722086

2073-
if _, err := client.Namespaces().Create(
2074-
context.TODO(),
2075-
&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-client-415"}},
2076-
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
2077-
); !apierrors.IsUnsupportedMediaType(err) {
2078-
t.Errorf("expected to receive unsupported media type on cbor request with fresh client, got: %v", err)
2087+
if _, err := client.Namespaces().Create(
2088+
context.TODO(),
2089+
&v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test-client-415"}},
2090+
metav1.CreateOptions{DryRun: []string{metav1.DryRunAll}},
2091+
); !apierrors.IsUnsupportedMediaType(err) {
2092+
t.Errorf("expected to receive unsupported media type on cbor request with fresh client, got: %v", err)
2093+
}
2094+
})
20792095
}
20802096
}

0 commit comments

Comments
 (0)