From 2870bbf8328f905b0126bcfa065c919f5aef5e28 Mon Sep 17 00:00:00 2001 From: jennybuckley Date: Wed, 16 May 2018 15:25:25 -0700 Subject: [PATCH] Implement first iteration of serverside apply as a PUT --- .../k8s.io/apimachinery/pkg/runtime/types.go | 1 + .../k8s.io/apimachinery/pkg/types/patch.go | 1 + .../src/k8s.io/apiserver/pkg/endpoints/BUILD | 2 + .../apiserver/pkg/endpoints/handlers/patch.go | 56 ++++++++++++++----- .../pkg/endpoints/handlers/rest_test.go | 2 +- .../apiserver/pkg/endpoints/installer.go | 7 ++- 6 files changed, 53 insertions(+), 16 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go index e4515d8ed00..1f7f662e075 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/types.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/types.go @@ -42,6 +42,7 @@ type TypeMeta struct { const ( ContentTypeJSON string = "application/json" + ContentTypeYAML string = "application/yaml" ) // RawExtension is used to hold extensions in external versions. diff --git a/staging/src/k8s.io/apimachinery/pkg/types/patch.go b/staging/src/k8s.io/apimachinery/pkg/types/patch.go index d522d1dbdc6..fe8ecaaffa6 100644 --- a/staging/src/k8s.io/apimachinery/pkg/types/patch.go +++ b/staging/src/k8s.io/apimachinery/pkg/types/patch.go @@ -25,4 +25,5 @@ const ( JSONPatchType PatchType = "application/json-patch+json" MergePatchType PatchType = "application/merge-patch+json" StrategicMergePatchType PatchType = "application/strategic-merge-patch+json" + ApplyPatchType PatchType = "application/apply-patch+yaml" ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD index f0a07eea4e1..d74818937cc 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/BUILD @@ -78,8 +78,10 @@ go_library( "//vendor/k8s.io/apiserver/pkg/endpoints/handlers:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/handlers/negotiation:go_default_library", "//vendor/k8s.io/apiserver/pkg/endpoints/metrics:go_default_library", + "//vendor/k8s.io/apiserver/pkg/features:go_default_library", "//vendor/k8s.io/apiserver/pkg/registry/rest:go_default_library", "//vendor/k8s.io/apiserver/pkg/server/filters:go_default_library", + "//vendor/k8s.io/apiserver/pkg/util/feature:go_default_library", ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go index 0ed6dee7cb4..796315f381e 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -83,19 +83,23 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface return } - patchJS, err := readBody(req) + patchBytes, err := readBody(req) if err != nil { scope.err(err, w, req) return } ae := request.AuditEventFrom(ctx) - audit.LogRequestPatch(ae, patchJS) + audit.LogRequestPatch(ae, patchBytes) trace.Step("Recorded the audit event") - s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), runtime.ContentTypeJSON) + baseContentType := runtime.ContentTypeJSON + if patchType == types.ApplyPatchType { + baseContentType = runtime.ContentTypeYAML + } + s, ok := runtime.SerializerInfoForMediaType(scope.Serializer.SupportedMediaTypes(), baseContentType) if !ok { - scope.err(fmt.Errorf("no serializer defined for JSON"), w, req) + scope.err(fmt.Errorf("no serializer defined for %v", baseContentType), w, req) return } gv := scope.Kind.GroupVersion() @@ -134,7 +138,7 @@ func PatchResource(r rest.Patcher, scope RequestScope, admit admission.Interface restPatcher: r, name: name, patchType: patchType, - patchJS: patchJS, + patchBytes: patchBytes, trace: trace, } @@ -193,7 +197,7 @@ type patcher struct { restPatcher rest.Patcher name string patchType types.PatchType - patchJS []byte + patchBytes []byte trace *utiltrace.Trace @@ -238,18 +242,18 @@ func (p *jsonPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (r return objToUpdate, nil } -// patchJS applies the patch. Input and output objects must both have +// applyJSPatch applies the patch. Input and output objects must both have // the external version, since that is what the patch must have been constructed against. func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr error) { switch p.patchType { case types.JSONPatchType: - patchObj, err := jsonpatch.DecodePatch(p.patchJS) + patchObj, err := jsonpatch.DecodePatch(p.patchBytes) if err != nil { return nil, err } return patchObj.Apply(versionedJS) case types.MergePatchType: - return jsonpatch.MergePatch(versionedJS, p.patchJS) + return jsonpatch.MergePatch(versionedJS, p.patchBytes) default: // only here as a safety net - go-restful filters content-type return nil, fmt.Errorf("unknown Content-Type header for patch: %v", p.patchType) @@ -271,7 +275,7 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru if err != nil { return nil, err } - if err := strategicPatchObject(p.codec, p.defaulter, currentVersionedObject, p.patchJS, versionedObjToUpdate, p.schemaReferenceObj); err != nil { + if err := strategicPatchObject(p.codec, p.defaulter, currentVersionedObject, p.patchBytes, versionedObjToUpdate, p.schemaReferenceObj); err != nil { return nil, err } // Convert the object back to unversioned (aka internal version). @@ -283,16 +287,16 @@ func (p *smpPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (ru return unversionedObjToUpdate, nil } -// strategicPatchObject applies a strategic merge patch of to +// strategicPatchObject applies a strategic merge patch of to // and stores the result in . // It additionally returns the map[string]interface{} representation of the -// and . +// and . // NOTE: Both and are supposed to be versioned. func strategicPatchObject( codec runtime.Codec, defaulter runtime.ObjectDefaulter, originalObject runtime.Object, - patchJS []byte, + patchBytes []byte, objToUpdate runtime.Object, schemaReferenceObj runtime.Object, ) error { @@ -302,7 +306,7 @@ func strategicPatchObject( } patchMap := make(map[string]interface{}) - if err := json.Unmarshal(patchJS, &patchMap); err != nil { + if err := json.Unmarshal(patchBytes, &patchMap); err != nil { return errors.NewBadRequest(err.Error()) } @@ -312,6 +316,27 @@ func strategicPatchObject( return nil } +type applyPatcher struct { + *patcher +} + +func (p *applyPatcher) applyPatchToCurrentObject(currentObject runtime.Object) (runtime.Object, error) { + versionedObjToUpdate, err := p.creater.New(p.kind) + if err != nil { + return nil, err + } + if err := runtime.DecodeInto(p.codec, p.patchBytes, versionedObjToUpdate); err != nil { + return nil, err + } + // For now, instead of performing a structured merge we will just attempt a PUT + // So we just convert the object back to unversioned (aka internal version). + unversionedObjToUpdate, err := p.toUnversioned(versionedObjToUpdate) + if err != nil { + return nil, err + } + return unversionedObjToUpdate, nil +} + // applyPatch is called every time GuaranteedUpdate asks for the updated object, // and is given the currently persisted object as input. func (p *patcher) applyPatch(_ context.Context, _, currentObject runtime.Object) (runtime.Object, error) { @@ -348,6 +373,9 @@ func (p *patcher) patchResource(ctx context.Context) (runtime.Object, error) { p.mechanism = &jsonPatcher{patcher: p} case types.StrategicMergePatchType: p.mechanism = &smpPatcher{patcher: p} + // this case is unreachable if ServerSideApply is not enabled because we will have already rejected the content type + case types.ApplyPatchType: + p.mechanism = &applyPatcher{patcher: p} default: return nil, fmt.Errorf("%v: unimplemented patch type", p.patchType) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go index aba84a0a7ed..5099fbaf06c 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/rest_test.go @@ -399,7 +399,7 @@ func (tc *patchTestCase) Run(t *testing.T) { restPatcher: testPatcher, name: name, patchType: patchType, - patchJS: patch, + patchBytes: patch, trace: utiltrace.New("Patch" + name), } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go index 9b6025b71ce..ef36157e4e7 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/installer.go @@ -38,8 +38,10 @@ import ( "k8s.io/apiserver/pkg/endpoints/handlers" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/metrics" + "k8s.io/apiserver/pkg/features" "k8s.io/apiserver/pkg/registry/rest" genericfilters "k8s.io/apiserver/pkg/server/filters" + utilfeature "k8s.io/apiserver/pkg/util/feature" ) const ( @@ -655,11 +657,14 @@ func (a *APIInstaller) registerResourceHandlers(path string, storage rest.Storag string(types.MergePatchType), string(types.StrategicMergePatchType), } + if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) { + supportedTypes = append(supportedTypes, string(types.ApplyPatchType)) + } handler := metrics.InstrumentRouteFunc(action.Verb, resource, subresource, requestScope, restfulPatchResource(patcher, reqScope, admit, supportedTypes)) route := ws.PATCH(action.Path).To(handler). Doc(doc). Param(ws.QueryParameter("pretty", "If 'true', then the output is pretty printed.")). - Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.StrategicMergePatchType)). + Consumes(supportedTypes...). Operation("patch"+namespaced+kind+strings.Title(subresource)+operationSuffix). Produces(append(storageMeta.ProducesMIMETypes(action.Verb), mediaTypes...)...). Returns(http.StatusOK, "OK", producedObject).