Skip to content

Commit a77f4c7

Browse files
committed
Fix content type fallback when a client defaults to CBOR.
With the ClientsAllowCBOR client-go feature gate enabled, a 415 response to a CBOR-encoded REST causes all subsequent requests from the client to fall back to a JSON request encoding. This mechanism had only worked as intended when CBOR was explicitly configured in the ClientContentConfig. When both ClientsAllowCBOR and ClientsPreferCBOR are enabled, an unconfigured (empty) content type defaults to CBOR instead of JSON. Both ways of configuring a client to use the CBOR request encoding are now subject to the same fallback mechanism.
1 parent c9024e7 commit a77f4c7

File tree

3 files changed

+77
-54
lines changed

3 files changed

+77
-54
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/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)