Skip to content

Commit 16f9fdc

Browse files
authored
Merge pull request kubernetes#128273 from benluddy/cbor-apply
KEP-4222: Support CBOR encoding for apply requests.
2 parents 6435489 + 41f55d7 commit 16f9fdc

File tree

25 files changed

+509
-192
lines changed

25 files changed

+509
-192
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,10 @@ func (r *crdHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
325325
supportedTypes := []string{
326326
string(types.JSONPatchType),
327327
string(types.MergePatchType),
328-
string(types.ApplyPatchType),
328+
string(types.ApplyYAMLPatchType),
329+
}
330+
if utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
331+
supportedTypes = append(supportedTypes, string(types.ApplyCBORPatchType))
329332
}
330333

331334
var handlerFunc http.HandlerFunc

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -186,15 +186,16 @@ func ValidateUpdateOptions(options *metav1.UpdateOptions) field.ErrorList {
186186

187187
func ValidatePatchOptions(options *metav1.PatchOptions, patchType types.PatchType) field.ErrorList {
188188
allErrs := field.ErrorList{}
189-
if patchType != types.ApplyPatchType {
190-
if options.Force != nil {
191-
allErrs = append(allErrs, field.Forbidden(field.NewPath("force"), "may not be specified for non-apply patch"))
192-
}
193-
} else {
189+
switch patchType {
190+
case types.ApplyYAMLPatchType, types.ApplyCBORPatchType:
194191
if options.FieldManager == "" {
195192
// This field is defaulted to "kubectl" by kubectl, but HAS TO be explicitly set by controllers.
196193
allErrs = append(allErrs, field.Required(field.NewPath("fieldManager"), "is required for apply patch"))
197194
}
195+
default:
196+
if options.Force != nil {
197+
allErrs = append(allErrs, field.Forbidden(field.NewPath("force"), "may not be specified for non-apply patch"))
198+
}
198199
}
199200
allErrs = append(allErrs, ValidateFieldManager(options.FieldManager, field.NewPath("fieldManager"))...)
200201
allErrs = append(allErrs, ValidateDryRun(field.NewPath("dryRun"), options.DryRun)...)

staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -141,12 +141,23 @@ func TestValidPatchOptions(t *testing.T) {
141141
Force: boolPtr(true),
142142
FieldManager: "kubectl",
143143
},
144-
patchType: types.ApplyPatchType,
144+
patchType: types.ApplyYAMLPatchType,
145145
}, {
146146
opts: metav1.PatchOptions{
147147
FieldManager: "kubectl",
148148
},
149-
patchType: types.ApplyPatchType,
149+
patchType: types.ApplyYAMLPatchType,
150+
}, {
151+
opts: metav1.PatchOptions{
152+
Force: boolPtr(true),
153+
FieldManager: "kubectl",
154+
},
155+
patchType: types.ApplyCBORPatchType,
156+
}, {
157+
opts: metav1.PatchOptions{
158+
FieldManager: "kubectl",
159+
},
160+
patchType: types.ApplyCBORPatchType,
150161
}, {
151162
opts: metav1.PatchOptions{},
152163
patchType: types.MergePatchType,
@@ -175,7 +186,12 @@ func TestInvalidPatchOptions(t *testing.T) {
175186
// missing manager
176187
{
177188
opts: metav1.PatchOptions{},
178-
patchType: types.ApplyPatchType,
189+
patchType: types.ApplyYAMLPatchType,
190+
},
191+
// missing manager
192+
{
193+
opts: metav1.PatchOptions{},
194+
patchType: types.ApplyCBORPatchType,
179195
},
180196
// force on non-apply
181197
{

staging/src/k8s.io/apimachinery/pkg/types/patch.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,7 @@ const (
2525
JSONPatchType PatchType = "application/json-patch+json"
2626
MergePatchType PatchType = "application/merge-patch+json"
2727
StrategicMergePatchType PatchType = "application/strategic-merge-patch+json"
28-
ApplyPatchType PatchType = "application/apply-patch+yaml"
28+
ApplyPatchType PatchType = ApplyYAMLPatchType
29+
ApplyYAMLPatchType PatchType = "application/apply-patch+yaml"
30+
ApplyCBORPatchType PatchType = "application/apply-patch+cbor"
2931
)

staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ import (
3535
"k8s.io/apimachinery/pkg/apis/meta/v1/validation"
3636
"k8s.io/apimachinery/pkg/runtime"
3737
"k8s.io/apimachinery/pkg/runtime/schema"
38+
cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct"
3839
"k8s.io/apimachinery/pkg/types"
3940
"k8s.io/apimachinery/pkg/util/managedfields"
4041
"k8s.io/apimachinery/pkg/util/mergepatch"
42+
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
4143
"k8s.io/apimachinery/pkg/util/sets"
4244
"k8s.io/apimachinery/pkg/util/strategicpatch"
4345
"k8s.io/apimachinery/pkg/util/validation/field"
@@ -50,8 +52,10 @@ import (
5052
requestmetrics "k8s.io/apiserver/pkg/endpoints/handlers/metrics"
5153
"k8s.io/apiserver/pkg/endpoints/handlers/negotiation"
5254
"k8s.io/apiserver/pkg/endpoints/request"
55+
"k8s.io/apiserver/pkg/features"
5356
"k8s.io/apiserver/pkg/registry/rest"
5457
"k8s.io/apiserver/pkg/util/dryrun"
58+
utilfeature "k8s.io/apiserver/pkg/util/feature"
5559
"k8s.io/component-base/tracing"
5660
)
5761

@@ -129,10 +133,25 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac
129133
audit.LogRequestPatch(req.Context(), patchBytes)
130134
span.AddEvent("Recorded the audit event")
131135

132-
baseContentType := runtime.ContentTypeJSON
133-
if patchType == types.ApplyPatchType {
136+
var baseContentType string
137+
switch patchType {
138+
case types.ApplyYAMLPatchType:
134139
baseContentType = runtime.ContentTypeYAML
140+
case types.ApplyCBORPatchType:
141+
if !utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
142+
// This request should have already been rejected by the
143+
// Content-Type allowlist check. Return 500 because assumptions are
144+
// already broken and the feature is not GA.
145+
utilruntime.HandleErrorWithContext(req.Context(), nil, "The patch content-type allowlist check should have made this unreachable.")
146+
scope.err(errors.NewInternalError(errors.NewInternalError(fmt.Errorf("unexpected patch type: %v", patchType))), w, req)
147+
return
148+
}
149+
150+
baseContentType = runtime.ContentTypeCBOR
151+
default:
152+
baseContentType = runtime.ContentTypeJSON
135153
}
154+
136155
s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), baseContentType)
137156
if !ok {
138157
scope.err(fmt.Errorf("no serializer defined for %v", baseContentType), w, req)
@@ -452,6 +471,20 @@ func (p *smpPatcher) createNewObject(_ context.Context) (runtime.Object, error)
452471
return nil, errors.NewNotFound(p.resource.GroupResource(), p.name)
453472
}
454473

474+
func newApplyPatcher(p *patcher, fieldManager *managedfields.FieldManager, unmarshalFn, unmarshalStrictFn func([]byte, interface{}) error) *applyPatcher {
475+
return &applyPatcher{
476+
fieldManager: fieldManager,
477+
patch: p.patchBytes,
478+
options: p.options,
479+
creater: p.creater,
480+
kind: p.kind,
481+
userAgent: p.userAgent,
482+
validationDirective: p.validationDirective,
483+
unmarshalFn: unmarshalFn,
484+
unmarshalStrictFn: unmarshalStrictFn,
485+
}
486+
}
487+
455488
type applyPatcher struct {
456489
patch []byte
457490
options *metav1.PatchOptions
@@ -460,6 +493,8 @@ type applyPatcher struct {
460493
fieldManager *managedfields.FieldManager
461494
userAgent string
462495
validationDirective string
496+
unmarshalFn func(data []byte, v interface{}) error
497+
unmarshalStrictFn func(data []byte, v interface{}) error
463498
}
464499

465500
func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context, obj runtime.Object) (runtime.Object, error) {
@@ -472,7 +507,7 @@ func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context,
472507
}
473508

474509
patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
475-
if err := yaml.Unmarshal(p.patch, &patchObj.Object); err != nil {
510+
if err := p.unmarshalFn(p.patch, &patchObj.Object); err != nil {
476511
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding YAML: %v", err))
477512
}
478513

@@ -484,7 +519,7 @@ func (p *applyPatcher) applyPatchToCurrentObject(requestContext context.Context,
484519
// TODO: spawn something to track deciding whether a fieldValidation=Strict
485520
// fatal error should return before an error from the apply operation
486521
if p.validationDirective == metav1.FieldValidationStrict || p.validationDirective == metav1.FieldValidationWarn {
487-
if err := yaml.UnmarshalStrict(p.patch, &map[string]interface{}{}); err != nil {
522+
if err := p.unmarshalStrictFn(p.patch, &map[string]interface{}{}); err != nil {
488523
if p.validationDirective == metav1.FieldValidationStrict {
489524
return nil, errors.NewBadRequest(fmt.Sprintf("error strict decoding YAML: %v", err))
490525
}
@@ -634,16 +669,21 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
634669
fieldManager: scope.FieldManager,
635670
}
636671
// this case is unreachable if ServerSideApply is not enabled because we will have already rejected the content type
637-
case types.ApplyPatchType:
638-
p.mechanism = &applyPatcher{
639-
fieldManager: scope.FieldManager,
640-
patch: p.patchBytes,
641-
options: p.options,
642-
creater: p.creater,
643-
kind: p.kind,
644-
userAgent: p.userAgent,
645-
validationDirective: p.validationDirective,
646-
}
672+
case types.ApplyYAMLPatchType:
673+
p.mechanism = newApplyPatcher(p, scope.FieldManager, yaml.Unmarshal, yaml.UnmarshalStrict)
674+
p.forceAllowCreate = true
675+
case types.ApplyCBORPatchType:
676+
if !utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
677+
utilruntime.HandleErrorWithContext(context.TODO(), nil, "CBOR apply requests should be rejected before reaching this point unless the feature gate is enabled.")
678+
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
679+
}
680+
681+
// The strict and non-strict funcs are the same here because any CBOR map with
682+
// duplicate keys is invalid and always rejected outright regardless of strictness
683+
// mode, and unknown field errors can't occur in practice because the type of the
684+
// destination value for unmarshaling an apply configuration is always
685+
// "unstructured".
686+
p.mechanism = newApplyPatcher(p, scope.FieldManager, cbor.Unmarshal, cbor.Unmarshal)
647687
p.forceAllowCreate = true
648688
default:
649689
return nil, false, fmt.Errorf("%v: unimplemented patch type", p.patchType)
@@ -670,7 +710,7 @@ func (p *patcher) patchResource(ctx context.Context, scope *RequestScope) (runti
670710
result, err := requestFunc()
671711
// If the object wasn't committed to storage because it's serialized size was too large,
672712
// it is safe to remove managedFields (which can be large) and try again.
673-
if isTooLargeError(err) && p.patchType != types.ApplyPatchType {
713+
if isTooLargeError(err) && p.patchType != types.ApplyYAMLPatchType && p.patchType != types.ApplyCBORPatchType {
674714
if _, accessorErr := meta.Accessor(p.restPatcher.New()); accessorErr == nil {
675715
p.updatedObjectInfo = rest.DefaultUpdatedObjectInfo(nil,
676716
p.applyPatch,

staging/src/k8s.io/apiserver/pkg/endpoints/installer.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -875,7 +875,10 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag
875875
string(types.JSONPatchType),
876876
string(types.MergePatchType),
877877
string(types.StrategicMergePatchType),
878-
string(types.ApplyPatchType),
878+
string(types.ApplyYAMLPatchType),
879+
}
880+
if utilfeature.TestOnlyFeatureGate.Enabled(features.TestOnlyCBORServingAndStorage) {
881+
supportedTypes = append(supportedTypes, string(types.ApplyCBORPatchType))
879882
}
880883
handler := metrics.InstrumentRouteFunc(action.Verb, group, version, resource, subresource, requestScope, metrics.APIServerComponent, deprecated, removedRelease, restfulPatchResource(patcher, reqScope, admit, supportedTypes))
881884
handler = utilwarning.AddWarningsHandler(handler, warnings)

staging/src/k8s.io/client-go/dynamic/golden_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func TestGoldenRequest(t *testing.T) {
207207
if err != nil {
208208
t.Fatalf("failed to load fixture: %v", err)
209209
}
210-
if diff := cmp.Diff(got, want); diff != "" {
210+
if diff := cmp.Diff(want, got); diff != "" {
211211
t.Errorf("unexpected difference from expected bytes:\n%s", diff)
212212
}
213213
}))

staging/src/k8s.io/client-go/dynamic/simple.go

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,12 @@ import (
2525
"k8s.io/apimachinery/pkg/api/meta"
2626
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2727
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
28-
"k8s.io/apimachinery/pkg/runtime"
2928
"k8s.io/apimachinery/pkg/runtime/schema"
3029
"k8s.io/apimachinery/pkg/types"
3130
"k8s.io/apimachinery/pkg/watch"
3231
"k8s.io/client-go/features"
3332
"k8s.io/client-go/rest"
33+
"k8s.io/client-go/util/apply"
3434
"k8s.io/client-go/util/consistencydetector"
3535
"k8s.io/client-go/util/watchlist"
3636
"k8s.io/klog/v2"
@@ -340,10 +340,6 @@ func (c *dynamicResourceClient) Apply(ctx context.Context, name string, obj *uns
340340
if err := validateNamespaceWithOptionalName(c.namespace, name); err != nil {
341341
return nil, err
342342
}
343-
outBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj)
344-
if err != nil {
345-
return nil, err
346-
}
347343
accessor, err := meta.Accessor(obj)
348344
if err != nil {
349345
return nil, err
@@ -355,21 +351,16 @@ func (c *dynamicResourceClient) Apply(ctx context.Context, name string, obj *uns
355351
}
356352
patchOpts := opts.ToPatchOptions()
357353

358-
result := c.client.client.
359-
Patch(types.ApplyPatchType).
360-
AbsPath(append(c.makeURLSegments(name), subresources...)...).
361-
Body(outBytes).
362-
SpecificallyVersionedParams(&patchOpts, dynamicParameterCodec, versionV1).
363-
Do(ctx)
364-
if err := result.Error(); err != nil {
365-
return nil, err
366-
}
367-
retBytes, err := result.Raw()
354+
request, err := apply.NewRequest(c.client.client, obj.Object)
368355
if err != nil {
369356
return nil, err
370357
}
358+
371359
var out unstructured.Unstructured
372-
if err := runtime.DecodeInto(unstructured.UnstructuredJSONScheme, retBytes, &out); err != nil {
360+
if err := request.
361+
AbsPath(append(c.makeURLSegments(name), subresources...)...).
362+
SpecificallyVersionedParams(&patchOpts, dynamicParameterCodec, versionV1).
363+
Do(ctx).Into(&out); err != nil {
373364
return nil, err
374365
}
375366
return &out, nil

staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/apply

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/fin?force=true HTTP/1.1
22
Host: example.com
33
Accept: application/json
44
Accept-Encoding: gzip
5-
Content-Length: 29
5+
Content-Length: 28
66
Content-Type: application/apply-patch+yaml
77
User-Agent: TestGoldenRequest
88

9-
{"metadata":{"name":"mips"}}
9+
{"metadata":{"name":"mips"}}

staging/src/k8s.io/client-go/dynamic/testdata/TestGoldenRequest/applystatus

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ PATCH /apis/flops/v1alpha1/namespaces/mops/flips/mips/status?force=true HTTP/1.1
22
Host: example.com
33
Accept: application/json
44
Accept-Encoding: gzip
5-
Content-Length: 29
5+
Content-Length: 28
66
Content-Type: application/apply-patch+yaml
77
User-Agent: TestGoldenRequest
88

9-
{"metadata":{"name":"mips"}}
9+
{"metadata":{"name":"mips"}}

0 commit comments

Comments
 (0)