From f18d21d6ebf0170b73cdb3263385088d4e5368f3 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 14 Jan 2021 20:14:12 +0100 Subject: [PATCH 01/21] test managedFields admission validation --- ...er_test.go => duplicate_owner_ref_test.go} | 0 .../invalid_managedFields_test.go | 243 ++++++++++++++++++ 2 files changed, 243 insertions(+) rename test/integration/apiserver/admissionwebhook/{apiserver_handler_test.go => duplicate_owner_ref_test.go} (100%) create mode 100644 test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go diff --git a/test/integration/apiserver/admissionwebhook/apiserver_handler_test.go b/test/integration/apiserver/admissionwebhook/duplicate_owner_ref_test.go similarity index 100% rename from test/integration/apiserver/admissionwebhook/apiserver_handler_test.go rename to test/integration/apiserver/admissionwebhook/duplicate_owner_ref_test.go diff --git a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go new file mode 100644 index 00000000000..85dfa096bd5 --- /dev/null +++ b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go @@ -0,0 +1,243 @@ +/* +Copyright 2020 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 admissionwebhook + +import ( + "bytes" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + v1 "k8s.io/api/admission/v1" + admissionv1 "k8s.io/api/admissionregistration/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" + clientset "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" + "k8s.io/kubernetes/test/integration/framework" +) + +// TestMutatingWebhookResetsInvalidManagedFields ensures that the API server +// resets managedFields to their state before admission if a mutating webhook +// patches create/update requests with invalid managedFields. +func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { + roots := x509.NewCertPool() + if !roots.AppendCertsFromPEM(localhostCert) { + t.Fatal("Failed to append Cert from PEM") + } + cert, err := tls.X509KeyPair(localhostCert, localhostKey) + if err != nil { + t.Fatalf("Failed to build cert with error: %+v", err) + } + + webhookServer := httptest.NewUnstartedServer(newInvalidManagedFieldsWebhookHandler(t)) + webhookServer.TLS = &tls.Config{ + RootCAs: roots, + Certificates: []tls.Certificate{cert}, + } + webhookServer.StartTLS() + defer webhookServer.Close() + + s := kubeapiservertesting.StartTestServerOrDie(t, + kubeapiservertesting.NewDefaultTestServerOptions(), []string{ + "--disable-admission-plugins=ServiceAccount", + }, framework.SharedEtcd()) + defer s.TearDownFn() + + recordedWarnings := &bytes.Buffer{} + warningWriter := restclient.NewWarningWriter(recordedWarnings, restclient.WarningWriterOptions{}) + s.ClientConfig.WarningHandler = warningWriter + client := clientset.NewForConfigOrDie(s.ClientConfig) + + if _, err := client.CoreV1().Pods("default").Create( + context.TODO(), invalidManagedFieldsMarkerFixture, metav1.CreateOptions{}); err != nil { + t.Fatal(err) + } + // make sure we delete the pod even on a failed test + defer func() { + if err := client.CoreV1().Pods("default").Delete(context.TODO(), invalidManagedFieldsMarkerFixture.Name, metav1.DeleteOptions{}); err != nil { + t.Fatalf("failed to delete marker pod: %v", err) + } + }() + + fail := admissionv1.Fail + none := admissionv1.SideEffectClassNone + mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1.MutatingWebhookConfiguration{ + ObjectMeta: metav1.ObjectMeta{Name: "invalid-managedfields.admission.integration.test"}, + Webhooks: []admissionv1.MutatingWebhook{{ + Name: "invalid-managedfields.admission.integration.test", + ClientConfig: admissionv1.WebhookClientConfig{ + URL: &webhookServer.URL, + CABundle: localhostCert, + }, + Rules: []admissionv1.RuleWithOperations{{ + Operations: []admissionv1.OperationType{admissionv1.Create, admissionv1.Update}, + Rule: admissionv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}}, + }}, + FailurePolicy: &fail, + AdmissionReviewVersions: []string{"v1", "v1beta1"}, + SideEffects: &none, + }}, + }, metav1.CreateOptions{}) + if err != nil { + t.Fatal(err) + } + defer func() { + err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{}) + if err != nil { + t.Fatal(err) + } + }() + + var pod *corev1.Pod + var lastErr string + // TODO(kwiesmueller): define warning format in the apiserver and use here + expectedWarning := fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat + + // Make sure reset happens on patch requests + // wait until new webhook is called + if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) { + pod, err = client.CoreV1().Pods("default").Patch(context.TODO(), invalidManagedFieldsMarkerFixture.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{}) + if err != nil { + return false, err + } + if warningWriter.WarningCount() == 0 { + lastErr = fmt.Sprintf("no warning, managedFields: %v", pod.ManagedFields) + return false, nil + } + if !strings.Contains(recordedWarnings.String(), expectedWarning) { + lastErr = fmt.Sprintf("unexpected warning, expected: %v, got: %v", + expectedWarning, recordedWarnings.String()) + return false, nil + } + if err := expectValidManagedFields(pod.ManagedFields); err != nil { + lastErr = err.Error() + return false, nil + } + return true, nil + }); err != nil { + t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr) + } + if warningWriter.WarningCount() != 1 { + t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) + } + recordedWarnings.Reset() + + // Make sure dedup happens in update requests + pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{}) + if err != nil { + t.Fatal(err) + } + if warningWriter.WarningCount() != 2 { + t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) + } + if !strings.Contains(recordedWarnings.String(), expectedWarning) { + t.Errorf("unexpected warning, expected: %v, got: %v", + expectedWarning, recordedWarnings.String()) + } + if err := expectValidManagedFields(pod.ManagedFields); err != nil { + t.Error(err) + } + recordedWarnings.Reset() + +} + +func expectValidManagedFields(managedFields []metav1.ManagedFieldsEntry) error { + for i, managed := range managedFields { + // TODO: define what we want to validate + if len(managed.APIVersion) < 1 { + return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing apiVersion", i) + } + if len(managed.FieldsType) < 1 { + return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing fieldsType", i) + } + if len(managed.Manager) < 1 { + return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing manager", i) + } + if managed.FieldsV1 == nil { + return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing fieldsV1", i) + } + } + return nil +} + +func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + data, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + review := v1.AdmissionReview{} + if err := json.Unmarshal(data, &review); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + if len(review.Request.Object.Raw) == 0 { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + pod := &corev1.Pod{} + if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + review.Response = &v1.AdmissionResponse{ + Allowed: true, + UID: review.Request.UID, + Result: &metav1.Status{Message: "admitted"}, + } + + if len(pod.ManagedFields) != 0 { + t.Logf("corrupting managedFields %v", pod.ManagedFields) + review.Response.Patch = []byte(`[{"op":"remove","path":"metadata/managedFields/0/apiVersion"},{"op":"remove","path":"/metadata/managedFields/0/fieldsType"}]`) + jsonPatch := v1.PatchTypeJSONPatch + review.Response.PatchType = &jsonPatch + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(review); err != nil { + t.Errorf("Marshal of response failed with error: %v", err) + } + }) +} + +var invalidManagedFieldsMarkerFixture = &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "invalid-managedfields-test-marker", + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "fake-name", + Image: "fakeimage", + }}, + }, +} From d5ae113e8dfba62709b1fccb8dbc26c6dde9b3e5 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 14 Jan 2021 20:14:38 +0100 Subject: [PATCH 02/21] implement managedFields admission controller --- .../handlers/fieldmanager/admission.go | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go new file mode 100644 index 00000000000..8727f47bc4d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -0,0 +1,105 @@ +/* +Copyright 2021 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 fieldmanager + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/warning" +) + +// InvalidManagedFieldsAfterMutatingAdmissionWarningFormat is the warning that a client receives +// when a create/update/patch request results in invalid managedFields after going through the admission chain. +const InvalidManagedFieldsAfterMutatingAdmissionWarningFormat = ".metadata.managedFields was in an invalid state after admission; this could be caused by an outdated mutating admission controller; please fix your requests: %v" + +// NewManagedFieldsValidatingAdmissionController validates the managedFields after calling +// the provided admission and resets them to their original state if they got changed to an invalid value +func NewManagedFieldsValidatingAdmissionController(wrap admission.Interface) admission.Interface { + return nil +} + +type managedFieldsValidatingAdmissionController struct { + wrap admission.Interface +} + +var _ admission.Interface = &managedFieldsValidatingAdmissionController{} +var _ admission.MutationInterface = &managedFieldsValidatingAdmissionController{} +var _ admission.ValidationInterface = &managedFieldsValidatingAdmissionController{} + +// Handles calls the wrapped admission.Interface if aplicable +func (admit *managedFieldsValidatingAdmissionController) Handles(operation admission.Operation) bool { + return admit.wrap.Handles(operation) +} + +// Admit calls the wrapped admission.Interface if aplicable and resets the managedFields to their state before admission if they +// got modified in an invalid way +func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + mutationInterface, isMutationInterface := admit.wrap.(admission.MutationInterface) + if !isMutationInterface { + return nil + } + objectMeta, err := meta.Accessor(a.GetObject()) + if err != nil { + return err + } + managedFieldsBeforeAdmission := objectMeta.GetManagedFields() + if err := mutationInterface.Admit(ctx, a, o); err != nil { + return err + } + objectMeta, err = meta.Accessor(a.GetObject()) + if err != nil { + return err + } + managedFieldsAfterAdmission := objectMeta.GetManagedFields() + if err := validateManagedFields(managedFieldsAfterAdmission); err != nil { + objectMeta.SetManagedFields(managedFieldsBeforeAdmission) + warning.AddWarning(ctx, "", + fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, + err.Error()), + ) + } + return nil +} + +// Validate calls the wrapped admission.Interface if aplicable +func (admit *managedFieldsValidatingAdmissionController) Validate(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { + if validationInterface, isValidationInterface := admit.wrap.(admission.ValidationInterface); isValidationInterface { + return validationInterface.Validate(ctx, a, o) + } + return nil +} + +func validateManagedFields(managedFields []metav1.ManagedFieldsEntry) error { + for i, managed := range managedFields { + if len(managed.APIVersion) < 1 { + return fmt.Errorf(".metadata.managedFields[%d]: missing apiVersion", i) + } + if len(managed.FieldsType) < 1 { + return fmt.Errorf(".metadata.managedFields[%d]: missing fieldsType", i) + } + if len(managed.Manager) < 1 { + return fmt.Errorf(".metadata.managedFields[%d]: missing manager", i) + } + if managed.FieldsV1 == nil { + return fmt.Errorf(".metadata.managedFields[%d]: missing fieldsV1", i) + } + } + return nil +} From 3d306e222de3b13a55030a53fef93622bb300646 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 14 Jan 2021 20:15:11 +0100 Subject: [PATCH 03/21] use managedFields admission controller in create/patch/update --- staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go | 3 +++ staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go | 2 +- staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index e914eaeea5d..743f289483c 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -163,6 +164,8 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return nil, fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err) } obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent())) + + admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) } if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) { if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil { 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 4c0d102308a..b8e2991a986 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -171,7 +171,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac userInfo, ) - mutatingAdmission, _ := admit.(admission.MutationInterface) + mutatingAdmission, _ := fieldmanager.NewManagedFieldsValidatingAdmissionController(admit).(admission.MutationInterface) createAuthorizerAttributes := authorizer.AttributesRecord{ User: userInfo, ResourceRequest: true, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index fd215bb38af..d6042aa10a5 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -33,6 +33,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -118,6 +119,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa ae := request.AuditEventFrom(ctx) audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource, scope.Serializer) admit = admission.WithAudit(admit, ae) + admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) if err := checkName(obj, name, namespace, scope.Namer); err != nil { scope.err(err, w, req) From a06f981fb1388976cd6427d7a4284d36dd2f2448 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 14 Jan 2021 20:33:50 +0100 Subject: [PATCH 04/21] update bazel --- .../apiserver/pkg/endpoints/handlers/fieldmanager/admission.go | 1 + 1 file changed, 1 insertion(+) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index 8727f47bc4d..10c172adf58 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -13,6 +13,7 @@ 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 fieldmanager import ( From ffbae9c5b4a3df9f7400d0047a5d7a957adf295d Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Mon, 1 Feb 2021 17:56:49 +0100 Subject: [PATCH 05/21] disable webhook for testing --- .../pkg/endpoints/handlers/create.go | 3 -- .../apiserver/pkg/endpoints/handlers/patch.go | 2 +- .../pkg/endpoints/handlers/update.go | 2 - .../invalid_managedFields_test.go | 48 +++++++++---------- 4 files changed, 24 insertions(+), 31 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index 743f289483c..e914eaeea5d 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -35,7 +35,6 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -164,8 +163,6 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return nil, fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err) } obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent())) - - admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) } if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) { if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil { 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 b8e2991a986..4c0d102308a 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -171,7 +171,7 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac userInfo, ) - mutatingAdmission, _ := fieldmanager.NewManagedFieldsValidatingAdmissionController(admit).(admission.MutationInterface) + mutatingAdmission, _ := admit.(admission.MutationInterface) createAuthorizerAttributes := authorizer.AttributesRecord{ User: userInfo, ResourceRequest: true, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index d6042aa10a5..fd215bb38af 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -33,7 +33,6 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -119,7 +118,6 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa ae := request.AuditEventFrom(ctx) audit.LogRequestObject(ae, obj, scope.Resource, scope.Subresource, scope.Serializer) admit = admission.WithAudit(admit, ae) - admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) if err := checkName(obj, name, namespace, scope.Namer); err != nil { scope.err(err, w, req) diff --git a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go index 85dfa096bd5..cd2499f9f91 100644 --- a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go +++ b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go @@ -26,7 +26,6 @@ import ( "io/ioutil" "net/http" "net/http/httptest" - "strings" "testing" "time" @@ -36,7 +35,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" @@ -118,7 +116,7 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { var pod *corev1.Pod var lastErr string // TODO(kwiesmueller): define warning format in the apiserver and use here - expectedWarning := fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat + // expectedWarning := fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat // Make sure reset happens on patch requests // wait until new webhook is called @@ -127,15 +125,15 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { if err != nil { return false, err } - if warningWriter.WarningCount() == 0 { - lastErr = fmt.Sprintf("no warning, managedFields: %v", pod.ManagedFields) - return false, nil - } - if !strings.Contains(recordedWarnings.String(), expectedWarning) { - lastErr = fmt.Sprintf("unexpected warning, expected: %v, got: %v", - expectedWarning, recordedWarnings.String()) - return false, nil - } + // if warningWriter.WarningCount() == 0 { + // lastErr = fmt.Sprintf("no warning, managedFields: %v", pod.ManagedFields) + // return false, nil + // } + // if !strings.Contains(recordedWarnings.String(), expectedWarning) { + // lastErr = fmt.Sprintf("unexpected warning, expected: %v, got: %v", + // expectedWarning, recordedWarnings.String()) + // return false, nil + // } if err := expectValidManagedFields(pod.ManagedFields); err != nil { lastErr = err.Error() return false, nil @@ -144,27 +142,27 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { }); err != nil { t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr) } - if warningWriter.WarningCount() != 1 { - t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) - } - recordedWarnings.Reset() + // if warningWriter.WarningCount() != 1 { + // t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) + // } + // recordedWarnings.Reset() // Make sure dedup happens in update requests pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{}) if err != nil { t.Fatal(err) } - if warningWriter.WarningCount() != 2 { - t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) - } - if !strings.Contains(recordedWarnings.String(), expectedWarning) { - t.Errorf("unexpected warning, expected: %v, got: %v", - expectedWarning, recordedWarnings.String()) - } + // if warningWriter.WarningCount() != 2 { + // t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) + // } + // if !strings.Contains(recordedWarnings.String(), expectedWarning) { + // t.Errorf("unexpected warning, expected: %v, got: %v", + // expectedWarning, recordedWarnings.String()) + // } if err := expectValidManagedFields(pod.ManagedFields); err != nil { t.Error(err) } - recordedWarnings.Reset() + // recordedWarnings.Reset() } @@ -217,7 +215,7 @@ func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler { if len(pod.ManagedFields) != 0 { t.Logf("corrupting managedFields %v", pod.ManagedFields) - review.Response.Patch = []byte(`[{"op":"remove","path":"metadata/managedFields/0/apiVersion"},{"op":"remove","path":"/metadata/managedFields/0/fieldsType"}]`) + review.Response.Patch = []byte(`[{"op":"remove","path":"/metadata/managedFields/0/apiVersion"},{"op":"remove","path":"/metadata/managedFields/0/fieldsType"}]`) jsonPatch := v1.PatchTypeJSONPatch review.Response.PatchType = &jsonPatch } From 711f2dab47d85ec61cdca0066d2b35e49fcabc6f Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 11 Feb 2021 16:11:20 +0100 Subject: [PATCH 06/21] fix test --- .../admissionwebhook/invalid_managedFields_test.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go index cd2499f9f91..67d2f4dbb7c 100644 --- a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go +++ b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go @@ -114,7 +114,7 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { }() var pod *corev1.Pod - var lastErr string + var lastErr error // TODO(kwiesmueller): define warning format in the apiserver and use here // expectedWarning := fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat @@ -135,13 +135,16 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { // return false, nil // } if err := expectValidManagedFields(pod.ManagedFields); err != nil { - lastErr = err.Error() + lastErr = err return false, nil } return true, nil }); err != nil { t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr) } + if lastErr != nil { + t.Fatal(lastErr) + } // if warningWriter.WarningCount() != 1 { // t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) // } @@ -215,7 +218,11 @@ func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler { if len(pod.ManagedFields) != 0 { t.Logf("corrupting managedFields %v", pod.ManagedFields) - review.Response.Patch = []byte(`[{"op":"remove","path":"/metadata/managedFields/0/apiVersion"},{"op":"remove","path":"/metadata/managedFields/0/fieldsType"}]`) + review.Response.Patch = []byte(`[ + {"op":"remove","path":"/metadata/managedFields/0/apiVersion"}, + {"op":"remove","path":"/metadata/managedFields/0/fieldsV1"}, + {"op":"remove","path":"/metadata/managedFields/0/fieldsType"} + ]`) jsonPatch := v1.PatchTypeJSONPatch review.Response.PatchType = &jsonPatch } From 429a96da5e856c435b08b50791d462120724c475 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 11 Feb 2021 16:11:43 +0100 Subject: [PATCH 07/21] fix admission controller --- .../pkg/endpoints/handlers/fieldmanager/admission.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index 10c172adf58..d489fda6502 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -33,7 +33,7 @@ const InvalidManagedFieldsAfterMutatingAdmissionWarningFormat = ".metadata.manag // NewManagedFieldsValidatingAdmissionController validates the managedFields after calling // the provided admission and resets them to their original state if they got changed to an invalid value func NewManagedFieldsValidatingAdmissionController(wrap admission.Interface) admission.Interface { - return nil + return &managedFieldsValidatingAdmissionController{wrap: wrap} } type managedFieldsValidatingAdmissionController struct { @@ -64,10 +64,6 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte if err := mutationInterface.Admit(ctx, a, o); err != nil { return err } - objectMeta, err = meta.Accessor(a.GetObject()) - if err != nil { - return err - } managedFieldsAfterAdmission := objectMeta.GetManagedFields() if err := validateManagedFields(managedFieldsAfterAdmission); err != nil { objectMeta.SetManagedFields(managedFieldsBeforeAdmission) From f86b59ab79227929e7f283b859b4c59317399807 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 11 Feb 2021 16:22:16 +0100 Subject: [PATCH 08/21] add managedFields admission --- staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go | 2 ++ staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go | 3 +++ staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go | 2 ++ 3 files changed, 7 insertions(+) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go index e914eaeea5d..b2e167f26fd 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/create.go @@ -35,6 +35,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -163,6 +164,7 @@ func createHandler(r rest.NamedCreater, scope *RequestScope, admit admission.Int return nil, fmt.Errorf("failed to create new object (Create for %v): %v", scope.Kind, err) } obj = scope.FieldManager.UpdateNoErrors(liveObj, obj, managerOrUserAgent(options.FieldManager, req.UserAgent())) + admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) } if mutatingAdmission, ok := admit.(admission.MutationInterface); ok && mutatingAdmission.Handles(admission.Create) { if err := mutatingAdmission.Admit(ctx, admissionAttributes, scope); err != nil { 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 4c0d102308a..903307fc285 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/patch.go @@ -171,6 +171,9 @@ func PatchResource(r rest.Patcher, scope *RequestScope, admit admission.Interfac userInfo, ) + if scope.FieldManager != nil { + admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) + } mutatingAdmission, _ := admit.(admission.MutationInterface) createAuthorizerAttributes := authorizer.AttributesRecord{ User: userInfo, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go index fd215bb38af..57daefd9cf1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/update.go @@ -33,6 +33,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/audit" "k8s.io/apiserver/pkg/authorization/authorizer" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" "k8s.io/apiserver/pkg/endpoints/handlers/negotiation" "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/apiserver/pkg/features" @@ -130,6 +131,7 @@ func UpdateResource(r rest.Updater, scope *RequestScope, admit admission.Interfa // allows skipping managedFields update if the resulting object is too big shouldUpdateManagedFields := true if scope.FieldManager != nil { + admit = fieldmanager.NewManagedFieldsValidatingAdmissionController(admit) transformers = append(transformers, func(_ context.Context, newObj, liveObj runtime.Object) (runtime.Object, error) { if shouldUpdateManagedFields { return scope.FieldManager.UpdateNoErrors(liveObj, newObj, managerOrUserAgent(options.FieldManager, req.UserAgent())), nil From 589ca1be1c9e75b1730feacd1af6e2c817f693ac Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 17:47:52 +0100 Subject: [PATCH 09/21] export and cleanup managedFields decoding --- .../handlers/fieldmanager/fieldmanager.go | 31 +++++++++++-------- .../fieldmanager/internal/managedfields.go | 24 +++++--------- .../internal/managedfields_test.go | 10 +++--- 3 files changed, 31 insertions(+), 34 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go index ec91d772fbc..71a4eff8e80 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/fieldmanager.go @@ -110,23 +110,22 @@ func newDefaultFieldManager(f Manager, typeConverter TypeConverter, objectConver return NewFieldManager(f, ignoreManagedFieldsFromRequestObject) } -func decodeLiveManagedFields(liveObj runtime.Object) (Managed, error) { +// DecodeManagedFields converts ManagedFields from the wire format (api format) +// to the format used by sigs.k8s.io/structured-merge-diff +func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (Managed, error) { + return internal.DecodeManagedFields(encodedManagedFields) +} + +func decodeLiveOrNew(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) { liveAccessor, err := meta.Accessor(liveObj) if err != nil { return nil, err } - managed, err := internal.DecodeObjectManagedFields(liveAccessor.GetManagedFields()) - if err != nil { - return internal.NewEmptyManaged(), nil - } - return managed, nil -} -func decodeManagedFields(liveObj, newObj runtime.Object, ignoreManagedFieldsFromRequestObject bool) (Managed, error) { // We take the managedFields of the live object in case the request tries to // manually set managedFields via a subresource. if ignoreManagedFieldsFromRequestObject { - return decodeLiveManagedFields(liveObj) + return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields())) } // If the object doesn't have metadata, we should just return without trying to @@ -140,14 +139,20 @@ func decodeManagedFields(liveObj, newObj runtime.Object, ignoreManagedFieldsFrom return internal.NewEmptyManaged(), nil } - managed, err := internal.DecodeObjectManagedFields(newAccessor.GetManagedFields()) // If the managed field is empty or we failed to decode it, // let's try the live object. This is to prevent clients who // don't understand managedFields from deleting it accidentally. + managed, err := DecodeManagedFields(newAccessor.GetManagedFields()) if err != nil || len(managed.Fields()) == 0 { - return decodeLiveManagedFields(liveObj) + return emptyManagedFieldsOnErr(DecodeManagedFields(liveAccessor.GetManagedFields())) } + return managed, nil +} +func emptyManagedFieldsOnErr(managed Managed, err error) (Managed, error) { + if err != nil { + return internal.NewEmptyManaged(), nil + } return managed, nil } @@ -157,7 +162,7 @@ func decodeManagedFields(liveObj, newObj runtime.Object, ignoreManagedFieldsFrom func (f *FieldManager) Update(liveObj, newObj runtime.Object, manager string) (object runtime.Object, err error) { // First try to decode the managed fields provided in the update, // This is necessary to allow directly updating managed fields. - managed, err := decodeManagedFields(liveObj, newObj, f.ignoreManagedFieldsFromRequestObject) + managed, err := decodeLiveOrNew(liveObj, newObj, f.ignoreManagedFieldsFromRequestObject) if err != nil { return newObj, nil } @@ -219,7 +224,7 @@ func (f *FieldManager) Apply(liveObj, appliedObj runtime.Object, manager string, } // Decode the managed fields in the live object, since it isn't allowed in the patch. - managed, err := internal.DecodeObjectManagedFields(accessor.GetManagedFields()) + managed, err := DecodeManagedFields(accessor.GetManagedFields()) if err != nil { return nil, fmt.Errorf("failed to decode managed fields: %v", err) } diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go index 9a625e2acf7..652ba64a839 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go @@ -77,15 +77,6 @@ func RemoveObjectManagedFields(obj runtime.Object) { accessor.SetManagedFields(nil) } -// DecodeObjectManagedFields extracts and converts the objects ManagedFields into a fieldpath.ManagedFields. -func DecodeObjectManagedFields(from []metav1.ManagedFieldsEntry) (ManagedInterface, error) { - managed, err := decodeManagedFields(from) - if err != nil { - return nil, fmt.Errorf("failed to convert managed fields from API: %v", err) - } - return &managed, nil -} - // EncodeObjectManagedFields converts and stores the fieldpathManagedFields into the objects ManagedFields func EncodeObjectManagedFields(obj runtime.Object, managed ManagedInterface) error { accessor, err := meta.Accessor(obj) @@ -102,9 +93,10 @@ func EncodeObjectManagedFields(obj runtime.Object, managed ManagedInterface) err return nil } -// decodeManagedFields converts ManagedFields from the wire format (api format) +// DecodeManagedFields converts ManagedFields from the wire format (api format) // to the format used by sigs.k8s.io/structured-merge-diff -func decodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (managed managedStruct, err error) { +func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (ManagedInterface, error) { + managed := managedStruct{} managed.fields = make(fieldpath.ManagedFields, len(encodedManagedFields)) managed.times = make(map[string]*metav1.Time, len(encodedManagedFields)) @@ -113,21 +105,21 @@ func decodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (mana case "FieldsV1": // Valid case. case "": - return managedStruct{}, fmt.Errorf("missing fieldsType in managed fields entry %d", i) + return nil, fmt.Errorf("missing fieldsType in managed fields entry %d", i) default: - return managedStruct{}, fmt.Errorf("invalid fieldsType %q in managed fields entry %d", encodedVersionedSet.FieldsType, i) + return nil, fmt.Errorf("invalid fieldsType %q in managed fields entry %d", encodedVersionedSet.FieldsType, i) } manager, err := BuildManagerIdentifier(&encodedVersionedSet) if err != nil { - return managedStruct{}, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err) + return nil, fmt.Errorf("error decoding manager from %v: %v", encodedVersionedSet, err) } managed.fields[manager], err = decodeVersionedSet(&encodedVersionedSet) if err != nil { - return managedStruct{}, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err) + return nil, fmt.Errorf("error decoding versioned set from %v: %v", encodedVersionedSet, err) } managed.times[manager] = encodedVersionedSet.Time } - return managed, nil + return &managed, nil } // BuildManagerIdentifier creates a manager identifier string from a ManagedFieldsEntry diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go index 68a6fa5a2b7..1ca4d4b0ba0 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go @@ -40,7 +40,7 @@ func TestHasFieldsType(t *testing.T) { `), &unmarshaled); err != nil { t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) } - if _, err := decodeManagedFields(unmarshaled); err != nil { + if _, err := DecodeManagedFields(unmarshaled); err != nil { t.Fatalf("did not expect decoding error but got: %v", err) } @@ -54,7 +54,7 @@ func TestHasFieldsType(t *testing.T) { `), &unmarshaled); err != nil { t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) } - if _, err := decodeManagedFields(unmarshaled); err == nil { + if _, err := DecodeManagedFields(unmarshaled); err == nil { t.Fatal("Expect decoding error but got none") } @@ -67,7 +67,7 @@ func TestHasFieldsType(t *testing.T) { `), &unmarshaled); err != nil { t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) } - if _, err := decodeManagedFields(unmarshaled); err == nil { + if _, err := DecodeManagedFields(unmarshaled); err == nil { t.Fatal("Expect decoding error but got none") } } @@ -189,11 +189,11 @@ func TestRoundTripManagedFields(t *testing.T) { if err := yaml.Unmarshal([]byte(test), &unmarshaled); err != nil { t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) } - decoded, err := decodeManagedFields(unmarshaled) + decoded, err := DecodeManagedFields(unmarshaled) if err != nil { t.Fatalf("did not expect decoding error but got: %v", err) } - encoded, err := encodeManagedFields(&decoded) + encoded, err := encodeManagedFields(decoded) if err != nil { t.Fatalf("did not expect encoding error but got: %v", err) } From da610d6667f3168ae205dd840220961377e1bc6d Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 20:02:34 +0100 Subject: [PATCH 10/21] consolidate managedFields validation --- .../apimachinery/pkg/api/validation/objectmeta.go | 7 +------ .../pkg/apis/meta/v1/validation/validation.go | 7 ++++++- .../pkg/apis/meta/v1/validation/validation_test.go | 11 +++++++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go b/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go index 889ec69aab8..7ac3a2bbddb 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go @@ -180,9 +180,7 @@ func ValidateObjectMetaAccessor(meta metav1.Object, requiresNamespace bool, name allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterName"), meta.GetClusterName(), msg)) } } - for _, entry := range meta.GetManagedFields() { - allErrs = append(allErrs, v1validation.ValidateFieldManager(entry.Manager, fldPath.Child("fieldManager"))...) - } + allErrs = append(allErrs, ValidateNonnegativeField(meta.GetGeneration(), fldPath.Child("generation"))...) allErrs = append(allErrs, v1validation.ValidateLabels(meta.GetLabels(), fldPath.Child("labels"))...) allErrs = append(allErrs, ValidateAnnotations(meta.GetAnnotations(), fldPath.Child("annotations"))...) @@ -248,9 +246,6 @@ func ValidateObjectMetaAccessorUpdate(newMeta, oldMeta metav1.Object, fldPath *f allErrs = append(allErrs, field.Invalid(fldPath.Child("generation"), newMeta.GetGeneration(), "must not be decremented")) } - for _, entry := range newMeta.GetManagedFields() { - allErrs = append(allErrs, v1validation.ValidateFieldManager(entry.Manager, fldPath.Child("fieldManager"))...) - } allErrs = append(allErrs, ValidateImmutableField(newMeta.GetName(), oldMeta.GetName(), fldPath.Child("name"))...) allErrs = append(allErrs, ValidateImmutableField(newMeta.GetNamespace(), oldMeta.GetNamespace(), fldPath.Child("namespace"))...) allErrs = append(allErrs, ValidateImmutableField(newMeta.GetUID(), oldMeta.GetUID(), fldPath.Child("uid"))...) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go index 715adf2f973..36760ef92ef 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go @@ -173,15 +173,20 @@ func ValidateTableOptions(opts *metav1.TableOptions) field.ErrorList { func ValidateManagedFields(fieldsList []metav1.ManagedFieldsEntry, fldPath *field.Path) field.ErrorList { var allErrs field.ErrorList - for _, fields := range fieldsList { + for i, fields := range fieldsList { + fldPath := fldPath.Index(i) switch fields.Operation { case metav1.ManagedFieldsOperationApply, metav1.ManagedFieldsOperationUpdate: default: allErrs = append(allErrs, field.Invalid(fldPath.Child("operation"), fields.Operation, "must be `Apply` or `Update`")) } + if len(fields.APIVersion) < 1 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVersion"), fields.APIVersion, "must not be empty")) + } if len(fields.FieldsType) > 0 && fields.FieldsType != "FieldsV1" { allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldsType"), fields.FieldsType, "must be `FieldsV1`")) } + allErrs = append(allErrs, ValidateFieldManager(fields.Manager, fldPath.Child("manager"))...) } return allErrs } diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go index 19cc93b6b7e..fd7f8be17db 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go @@ -256,6 +256,12 @@ func TestValidateManagedFieldsInvalid(t *testing.T) { // Operation is missing FieldsType: "FieldsV1", }, + { + Operation: metav1.ManagedFieldsOperationUpdate, + FieldsType: "FieldsV1", + // Invalid fieldManager + Manager: "field\nmanager", + }, } for _, test := range tests { @@ -282,6 +288,11 @@ func TestValidateMangedFieldsValid(t *testing.T) { Operation: metav1.ManagedFieldsOperationApply, FieldsType: "FieldsV1", }, + { + Operation: metav1.ManagedFieldsOperationApply, + FieldsType: "FieldsV1", + Manager: "🍔", + }, } for _, test := range tests { From fc1841d72f7418dd2606fb796f2a1b664bb3a721 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 20:03:10 +0100 Subject: [PATCH 11/21] use existing validation code and decoding in fieldManager admission --- .../handlers/fieldmanager/admission.go | 21 +--- .../handlers/fieldmanager/admission_test.go | 115 ++++++++++++++++++ .../handlers/fieldmanager/validation.go | 22 ++++ 3 files changed, 138 insertions(+), 20 deletions(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go create mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index d489fda6502..a276be9a7d2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -21,7 +21,6 @@ import ( "fmt" "k8s.io/apimachinery/pkg/api/meta" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/warning" ) @@ -65,7 +64,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte return err } managedFieldsAfterAdmission := objectMeta.GetManagedFields() - if err := validateManagedFields(managedFieldsAfterAdmission); err != nil { + if err := ValidateManagedFields(managedFieldsAfterAdmission); err != nil { objectMeta.SetManagedFields(managedFieldsBeforeAdmission) warning.AddWarning(ctx, "", fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, @@ -82,21 +81,3 @@ func (admit *managedFieldsValidatingAdmissionController) Validate(ctx context.Co } return nil } - -func validateManagedFields(managedFields []metav1.ManagedFieldsEntry) error { - for i, managed := range managedFields { - if len(managed.APIVersion) < 1 { - return fmt.Errorf(".metadata.managedFields[%d]: missing apiVersion", i) - } - if len(managed.FieldsType) < 1 { - return fmt.Errorf(".metadata.managedFields[%d]: missing fieldsType", i) - } - if len(managed.Manager) < 1 { - return fmt.Errorf(".metadata.managedFields[%d]: missing manager", i) - } - if managed.FieldsV1 == nil { - return fmt.Errorf(".metadata.managedFields[%d]: missing fieldsV1", i) - } - } - return nil -} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go new file mode 100644 index 00000000000..1f1c0ca1269 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go @@ -0,0 +1,115 @@ +package fieldmanager_test + +import ( + "context" + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apiserver/pkg/admission" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" + api "k8s.io/kubernetes/pkg/apis/core" +) + +func TestAdmission(t *testing.T) { + wrap := &mockAdmissionController{} + ac := fieldmanager.NewManagedFieldsValidatingAdmissionController(wrap) + + tests := []struct { + beforeAdmission []metav1.ManagedFieldsEntry + afterAdmission []metav1.ManagedFieldsEntry + expected []metav1.ManagedFieldsEntry + }{ + { + beforeAdmission: []metav1.ManagedFieldsEntry{ + { + Manager: "test", + }, + }, + afterAdmission: []metav1.ManagedFieldsEntry{ + { + Manager: "", + }, + }, + expected: []metav1.ManagedFieldsEntry{ + { + Manager: "test", + }, + }, + }, + { + beforeAdmission: []metav1.ManagedFieldsEntry{ + { + APIVersion: "test", + }, + }, + afterAdmission: []metav1.ManagedFieldsEntry{ + { + APIVersion: "", + }, + }, + expected: []metav1.ManagedFieldsEntry{ + { + APIVersion: "test", + }, + }, + }, + { + beforeAdmission: []metav1.ManagedFieldsEntry{ + { + FieldsType: "FieldsV1", + }, + }, + afterAdmission: []metav1.ManagedFieldsEntry{ + { + FieldsType: "test", + }, + }, + expected: []metav1.ManagedFieldsEntry{ + { + FieldsType: "FieldsV1", + }, + }, + }, + } + + for _, test := range tests { + obj := &unstructured.Unstructured{} + obj.SetManagedFields(test.beforeAdmission) + wrap.admit = replaceManagedFields(test.afterAdmission) + + attrs := admission.NewAttributesRecord(obj, obj, api.Kind("ConfigMap").WithVersion("version"), "default", "", api.Resource("configmaps").WithVersion("version"), "", admission.Update, nil, false, nil) + if err := ac.(admission.MutationInterface).Admit(context.TODO(), attrs, nil); err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(obj.GetManagedFields(), test.expected) { + t.Fatalf("expected: \n%v\ngot:\n%v", test.expected, obj.GetManagedFields()) + } + } +} + +func replaceManagedFields(with []metav1.ManagedFieldsEntry) func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + objectMeta, err := meta.Accessor(a.GetObject()) + if err != nil { + return err + } + objectMeta.SetManagedFields(with) + return nil + } +} + +type mockAdmissionController struct { + admit func(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error +} + +func (c *mockAdmissionController) Handles(operation admission.Operation) bool { + return true +} + +func (c *mockAdmissionController) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) error { + return c.admit(ctx, a, o) +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go new file mode 100644 index 00000000000..c862a7745bb --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go @@ -0,0 +1,22 @@ +package fieldmanager + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// ValidateManagedFields by checking the integrity of every entry and trying to decode +// them to the internal format +func ValidateManagedFields(managedFields []metav1.ManagedFieldsEntry) error { + validationErrs := v1validation.ValidateManagedFields(managedFields, field.NewPath("metadata").Child("managedFields")) + if len(validationErrs) > 0 { + return validationErrs.ToAggregate() + } + + if _, err := DecodeManagedFields(managedFields); err != nil { + return err + } + + return nil +} From ba2f6104f648caeba20596f8d016bec0d908171d Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 20:04:13 +0100 Subject: [PATCH 12/21] add warning check to managedFields integration test --- .../invalid_managedFields_test.go | 66 ++++++------------- 1 file changed, 21 insertions(+), 45 deletions(-) diff --git a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go index 67d2f4dbb7c..e6f9a9250f2 100644 --- a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go +++ b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go @@ -26,6 +26,7 @@ import ( "io/ioutil" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -35,6 +36,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" clientset "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" @@ -115,8 +117,8 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { var pod *corev1.Pod var lastErr error - // TODO(kwiesmueller): define warning format in the apiserver and use here - // expectedWarning := fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat + // We just expect the general warning text as the detail order might change + expectedWarning := fmt.Sprintf(fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, "") // Make sure reset happens on patch requests // wait until new webhook is called @@ -125,16 +127,7 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { if err != nil { return false, err } - // if warningWriter.WarningCount() == 0 { - // lastErr = fmt.Sprintf("no warning, managedFields: %v", pod.ManagedFields) - // return false, nil - // } - // if !strings.Contains(recordedWarnings.String(), expectedWarning) { - // lastErr = fmt.Sprintf("unexpected warning, expected: %v, got: %v", - // expectedWarning, recordedWarnings.String()) - // return false, nil - // } - if err := expectValidManagedFields(pod.ManagedFields); err != nil { + if err := fieldmanager.ValidateManagedFields(pod.ManagedFields); err != nil { lastErr = err return false, nil } @@ -145,47 +138,30 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { if lastErr != nil { t.Fatal(lastErr) } - // if warningWriter.WarningCount() != 1 { - // t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) - // } - // recordedWarnings.Reset() + if warningWriter.WarningCount() != 1 { + t.Errorf("expected one warning, got: %v", warningWriter.WarningCount()) + } + if !strings.Contains(recordedWarnings.String(), expectedWarning) { + t.Errorf("unexpected warning, expected: \n%v\n, got: \n%v", + expectedWarning, recordedWarnings.String()) + } + recordedWarnings.Reset() - // Make sure dedup happens in update requests + // Make sure reset happens in update requests pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{}) if err != nil { t.Fatal(err) } - // if warningWriter.WarningCount() != 2 { - // t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) - // } - // if !strings.Contains(recordedWarnings.String(), expectedWarning) { - // t.Errorf("unexpected warning, expected: %v, got: %v", - // expectedWarning, recordedWarnings.String()) - // } - if err := expectValidManagedFields(pod.ManagedFields); err != nil { + if err := fieldmanager.ValidateManagedFields(pod.ManagedFields); err != nil { t.Error(err) } - // recordedWarnings.Reset() - -} - -func expectValidManagedFields(managedFields []metav1.ManagedFieldsEntry) error { - for i, managed := range managedFields { - // TODO: define what we want to validate - if len(managed.APIVersion) < 1 { - return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing apiVersion", i) - } - if len(managed.FieldsType) < 1 { - return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing fieldsType", i) - } - if len(managed.Manager) < 1 { - return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing manager", i) - } - if managed.FieldsV1 == nil { - return fmt.Errorf(".metadata.managedFields[%d] is invalid: missing fieldsV1", i) - } + if warningWriter.WarningCount() != 2 { + t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount()) + } + if !strings.Contains(recordedWarnings.String(), expectedWarning) { + t.Errorf("unexpected warning, expected: \n%v\n, got: \n%v", + expectedWarning, recordedWarnings.String()) } - return nil } func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler { From 22dfa6ae1b431cb8e8afe72ce7d60360bf766337 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 20:14:11 +0100 Subject: [PATCH 13/21] prevent fieldManager admission from wrapping nil --- .../apiserver/pkg/endpoints/handlers/fieldmanager/admission.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index a276be9a7d2..751c4753d72 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -32,6 +32,9 @@ const InvalidManagedFieldsAfterMutatingAdmissionWarningFormat = ".metadata.manag // NewManagedFieldsValidatingAdmissionController validates the managedFields after calling // the provided admission and resets them to their original state if they got changed to an invalid value func NewManagedFieldsValidatingAdmissionController(wrap admission.Interface) admission.Interface { + if wrap == nil { + return nil + } return &managedFieldsValidatingAdmissionController{wrap: wrap} } From 1a8e2bf0358651151a6440717b4de1f662c4571b Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 20:14:22 +0100 Subject: [PATCH 14/21] update licenses and bazel --- .../pkg/api/validation/objectmeta.go | 2 +- .../handlers/fieldmanager/admission_test.go | 16 ++++++++++++++++ .../handlers/fieldmanager/validation.go | 16 ++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go b/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go index 7ac3a2bbddb..3e234eb11dc 100644 --- a/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go +++ b/staging/src/k8s.io/apimachinery/pkg/api/validation/objectmeta.go @@ -180,7 +180,7 @@ func ValidateObjectMetaAccessor(meta metav1.Object, requiresNamespace bool, name allErrs = append(allErrs, field.Invalid(fldPath.Child("clusterName"), meta.GetClusterName(), msg)) } } - + allErrs = append(allErrs, ValidateNonnegativeField(meta.GetGeneration(), fldPath.Child("generation"))...) allErrs = append(allErrs, v1validation.ValidateLabels(meta.GetLabels(), fldPath.Child("labels"))...) allErrs = append(allErrs, ValidateAnnotations(meta.GetAnnotations(), fldPath.Child("annotations"))...) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go index 1f1c0ca1269..ae51b9d1e13 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go @@ -1,3 +1,19 @@ +/* +Copyright 2021 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 fieldmanager_test import ( diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go index c862a7745bb..324c23b5553 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go @@ -1,3 +1,19 @@ +/* +Copyright 2021 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 fieldmanager import ( From 295e47f60b64332ef4e3268db282184357440675 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 20:33:36 +0100 Subject: [PATCH 15/21] fix test dependencies --- .../pkg/endpoints/handlers/fieldmanager/admission_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go index ae51b9d1e13..a6aac4448f2 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go @@ -24,9 +24,9 @@ import ( "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" - api "k8s.io/kubernetes/pkg/apis/core" ) func TestAdmission(t *testing.T) { @@ -96,7 +96,7 @@ func TestAdmission(t *testing.T) { obj.SetManagedFields(test.beforeAdmission) wrap.admit = replaceManagedFields(test.afterAdmission) - attrs := admission.NewAttributesRecord(obj, obj, api.Kind("ConfigMap").WithVersion("version"), "default", "", api.Resource("configmaps").WithVersion("version"), "", admission.Update, nil, false, nil) + attrs := admission.NewAttributesRecord(obj, obj, schema.GroupVersionKind{}, "default", "", schema.GroupVersionResource{}, "", admission.Update, nil, false, nil) if err := ac.(admission.MutationInterface).Admit(context.TODO(), attrs, nil); err != nil { t.Fatal(err) } From ff5a0ccb3650a6de29c8559aee10188b9457c2f3 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 26 Feb 2021 21:19:04 +0100 Subject: [PATCH 16/21] fix validation tests --- .../apis/meta/v1/validation/validation_test.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go index fd7f8be17db..33c42357418 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go @@ -247,20 +247,29 @@ func TestValidateManagedFieldsInvalid(t *testing.T) { { Operation: metav1.ManagedFieldsOperationUpdate, FieldsType: "RandomVersion", + APIVersion: "v1", }, { Operation: "RandomOperation", FieldsType: "FieldsV1", + APIVersion: "v1", }, { // Operation is missing FieldsType: "FieldsV1", + APIVersion: "v1", }, { Operation: metav1.ManagedFieldsOperationUpdate, FieldsType: "FieldsV1", // Invalid fieldManager - Manager: "field\nmanager", + Manager: "field\nmanager", + APIVersion: "v1", + }, + { + Operation: metav1.ManagedFieldsOperationUpdate, + FieldsType: "FieldsV1", + // APIVersion missing }, } @@ -277,20 +286,24 @@ func TestValidateManagedFieldsInvalid(t *testing.T) { func TestValidateMangedFieldsValid(t *testing.T) { tests := []metav1.ManagedFieldsEntry{ { - Operation: metav1.ManagedFieldsOperationUpdate, + Operation: metav1.ManagedFieldsOperationUpdate, + APIVersion: "v1", // FieldsType is missing }, { Operation: metav1.ManagedFieldsOperationUpdate, FieldsType: "FieldsV1", + APIVersion: "v1", }, { Operation: metav1.ManagedFieldsOperationApply, FieldsType: "FieldsV1", + APIVersion: "v1", }, { Operation: metav1.ManagedFieldsOperationApply, FieldsType: "FieldsV1", + APIVersion: "v1", Manager: "🍔", }, } From 470ad03d076cae44bc98c64a08eea32e65f1bb9f Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Mon, 1 Mar 2021 19:58:56 +0100 Subject: [PATCH 17/21] harden managedFields decoding --- .../fieldmanager/internal/managedfields.go | 8 ++ .../internal/managedfields_test.go | 78 +++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go index 652ba64a839..40237cdc660 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields.go @@ -101,6 +101,14 @@ func DecodeManagedFields(encodedManagedFields []metav1.ManagedFieldsEntry) (Mana managed.times = make(map[string]*metav1.Time, len(encodedManagedFields)) for i, encodedVersionedSet := range encodedManagedFields { + switch encodedVersionedSet.Operation { + case metav1.ManagedFieldsOperationApply, metav1.ManagedFieldsOperationUpdate: + default: + return nil, fmt.Errorf("operation must be `Apply` or `Update`") + } + if len(encodedVersionedSet.APIVersion) < 1 { + return nil, fmt.Errorf("apiVersion must not be empty") + } switch encodedVersionedSet.FieldsType { case "FieldsV1": // Valid case. diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go index 1ca4d4b0ba0..f6df7228c13 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal/managedfields_test.go @@ -72,6 +72,84 @@ func TestHasFieldsType(t *testing.T) { } } +// TestHasAPIVersion makes sure that we fail if we don't have an +// APIVersion set. +func TestHasAPIVersion(t *testing.T) { + var unmarshaled []metav1.ManagedFieldsEntry + if err := yaml.Unmarshal([]byte(`- apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:field: {} + manager: foo + operation: Apply +`), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + if _, err := DecodeManagedFields(unmarshaled); err != nil { + t.Fatalf("did not expect decoding error but got: %v", err) + } + + // Missing apiVersion. + unmarshaled = nil + if err := yaml.Unmarshal([]byte(`- fieldsType: FieldsV1 + fieldsV1: + f:field: {} + manager: foo + operation: Apply +`), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + if _, err := DecodeManagedFields(unmarshaled); err == nil { + t.Fatal("Expect decoding error but got none") + } +} + +// TestHasOperation makes sure that we fail if we don't have an +// Operation set properly. +func TestHasOperation(t *testing.T) { + var unmarshaled []metav1.ManagedFieldsEntry + if err := yaml.Unmarshal([]byte(`- apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:field: {} + manager: foo + operation: Apply +`), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + if _, err := DecodeManagedFields(unmarshaled); err != nil { + t.Fatalf("did not expect decoding error but got: %v", err) + } + + // Invalid operation. + if err := yaml.Unmarshal([]byte(`- apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:field: {} + manager: foo + operation: Invalid +`), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + if _, err := DecodeManagedFields(unmarshaled); err == nil { + t.Fatal("Expect decoding error but got none") + } + + // Missing operation. + unmarshaled = nil + if err := yaml.Unmarshal([]byte(`- apiVersion: v1 + fieldsType: FieldsV1 + fieldsV1: + f:field: {} + manager: foo +`), &unmarshaled); err != nil { + t.Fatalf("did not expect yaml unmarshalling error but got: %v", err) + } + if _, err := DecodeManagedFields(unmarshaled); err == nil { + t.Fatal("Expect decoding error but got none") + } +} + // TestRoundTripManagedFields will roundtrip ManagedFields from the wire format // (api format) to the format used by sigs.k8s.io/structured-merge-diff and back func TestRoundTripManagedFields(t *testing.T) { From 98d498117b5566c1229d754d97923f61600660d8 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Mon, 1 Mar 2021 20:29:15 +0100 Subject: [PATCH 18/21] only use managedFields decoding for admission check --- .../handlers/fieldmanager/admission.go | 2 +- .../handlers/fieldmanager/validation.go | 38 ------------------- .../invalid_managedFields_test.go | 16 +++++++- 3 files changed, 15 insertions(+), 41 deletions(-) delete mode 100644 staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index 751c4753d72..fdc95aa9331 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -67,7 +67,7 @@ func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Conte return err } managedFieldsAfterAdmission := objectMeta.GetManagedFields() - if err := ValidateManagedFields(managedFieldsAfterAdmission); err != nil { + if _, err := DecodeManagedFields(managedFieldsAfterAdmission); err != nil { objectMeta.SetManagedFields(managedFieldsBeforeAdmission) warning.AddWarning(ctx, "", fmt.Sprintf(InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go deleted file mode 100644 index 324c23b5553..00000000000 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/validation.go +++ /dev/null @@ -1,38 +0,0 @@ -/* -Copyright 2021 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 fieldmanager - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" - "k8s.io/apimachinery/pkg/util/validation/field" -) - -// ValidateManagedFields by checking the integrity of every entry and trying to decode -// them to the internal format -func ValidateManagedFields(managedFields []metav1.ManagedFieldsEntry) error { - validationErrs := v1validation.ValidateManagedFields(managedFields, field.NewPath("metadata").Child("managedFields")) - if len(validationErrs) > 0 { - return validationErrs.ToAggregate() - } - - if _, err := DecodeManagedFields(managedFields); err != nil { - return err - } - - return nil -} diff --git a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go index e6f9a9250f2..9a5ba8e8df3 100644 --- a/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go +++ b/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go @@ -34,7 +34,9 @@ import ( admissionv1 "k8s.io/api/admissionregistration/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" clientset "k8s.io/client-go/kubernetes" @@ -127,7 +129,7 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { if err != nil { return false, err } - if err := fieldmanager.ValidateManagedFields(pod.ManagedFields); err != nil { + if err := validateManagedFieldsAndDecode(pod.ManagedFields); err != nil { lastErr = err return false, nil } @@ -152,7 +154,7 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { if err != nil { t.Fatal(err) } - if err := fieldmanager.ValidateManagedFields(pod.ManagedFields); err != nil { + if err := validateManagedFieldsAndDecode(pod.ManagedFields); err != nil { t.Error(err) } if warningWriter.WarningCount() != 2 { @@ -164,6 +166,16 @@ func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) { } } +// validate against both decoding and validation to make sure we use the hardest rule between the both to reset +// with decoding being as strict as it gets, only using it should be enough in admission +func validateManagedFieldsAndDecode(managedFields []metav1.ManagedFieldsEntry) error { + if _, err := fieldmanager.DecodeManagedFields(managedFields); err != nil { + return err + } + validationErrs := v1validation.ValidateManagedFields(managedFields, field.NewPath("metadata").Child("managedFields")) + return validationErrs.ToAggregate() +} + func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() From ce0657cade72a693d4ff9348da7ea31818f389be Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 4 Mar 2021 21:17:14 +0100 Subject: [PATCH 19/21] remove apiVersion check from managedFields validation --- .../apimachinery/pkg/apis/meta/v1/validation/validation.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go index 36760ef92ef..a5a7f144add 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation.go @@ -180,9 +180,6 @@ func ValidateManagedFields(fieldsList []metav1.ManagedFieldsEntry, fldPath *fiel default: allErrs = append(allErrs, field.Invalid(fldPath.Child("operation"), fields.Operation, "must be `Apply` or `Update`")) } - if len(fields.APIVersion) < 1 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("apiVersion"), fields.APIVersion, "must not be empty")) - } if len(fields.FieldsType) > 0 && fields.FieldsType != "FieldsV1" { allErrs = append(allErrs, field.Invalid(fldPath.Child("fieldsType"), fields.FieldsType, "must be `FieldsV1`")) } From 2d1ba0c35829a2f146a712d49cb21f382c9894cb Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Thu, 4 Mar 2021 23:20:51 +0100 Subject: [PATCH 20/21] cleanup managedFields admission and test --- .../handlers/fieldmanager/admission.go | 4 +- .../handlers/fieldmanager/admission_test.go | 121 ++++++++---------- 2 files changed, 58 insertions(+), 67 deletions(-) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go index fdc95aa9331..20bbab06505 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission.go @@ -46,12 +46,12 @@ var _ admission.Interface = &managedFieldsValidatingAdmissionController{} var _ admission.MutationInterface = &managedFieldsValidatingAdmissionController{} var _ admission.ValidationInterface = &managedFieldsValidatingAdmissionController{} -// Handles calls the wrapped admission.Interface if aplicable +// Handles calls the wrapped admission.Interface if applicable func (admit *managedFieldsValidatingAdmissionController) Handles(operation admission.Operation) bool { return admit.wrap.Handles(operation) } -// Admit calls the wrapped admission.Interface if aplicable and resets the managedFields to their state before admission if they +// Admit calls the wrapped admission.Interface if applicable and resets the managedFields to their state before admission if they // got modified in an invalid way func (admit *managedFieldsValidatingAdmissionController) Admit(ctx context.Context, a admission.Attributes, o admission.ObjectInterfaces) (err error) { mutationInterface, isMutationInterface := admit.wrap.(admission.MutationInterface) diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go index a6aac4448f2..09df1f476c1 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/admission_test.go @@ -21,89 +21,80 @@ import ( "reflect" "testing" + v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager" + "k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager/internal" + "sigs.k8s.io/structured-merge-diff/v4/fieldpath" ) func TestAdmission(t *testing.T) { wrap := &mockAdmissionController{} ac := fieldmanager.NewManagedFieldsValidatingAdmissionController(wrap) + now := metav1.Now() - tests := []struct { - beforeAdmission []metav1.ManagedFieldsEntry - afterAdmission []metav1.ManagedFieldsEntry - expected []metav1.ManagedFieldsEntry - }{ - { - beforeAdmission: []metav1.ManagedFieldsEntry{ - { - Manager: "test", - }, - }, - afterAdmission: []metav1.ManagedFieldsEntry{ - { - Manager: "", - }, - }, - expected: []metav1.ManagedFieldsEntry{ - { - Manager: "test", - }, - }, + validFieldsV1, err := internal.SetToFields(*fieldpath.NewSet(fieldpath.MakePathOrDie("metadata", "labels", "test-label"))) + if err != nil { + t.Fatal(err) + } + validManagedFieldsEntry := metav1.ManagedFieldsEntry{ + APIVersion: "v1", + Operation: metav1.ManagedFieldsOperationApply, + Time: &now, + Manager: "test", + FieldsType: "FieldsV1", + FieldsV1: &validFieldsV1, + } + + managedFieldsMutators := map[string]func(in metav1.ManagedFieldsEntry) (out metav1.ManagedFieldsEntry, shouldReset bool){ + "invalid APIVersion": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { + managedFields.APIVersion = "" + return managedFields, true }, - { - beforeAdmission: []metav1.ManagedFieldsEntry{ - { - APIVersion: "test", - }, - }, - afterAdmission: []metav1.ManagedFieldsEntry{ - { - APIVersion: "", - }, - }, - expected: []metav1.ManagedFieldsEntry{ - { - APIVersion: "test", - }, - }, + "invalid Operation": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { + managedFields.Operation = "invalid operation" + return managedFields, true }, - { - beforeAdmission: []metav1.ManagedFieldsEntry{ - { - FieldsType: "FieldsV1", - }, - }, - afterAdmission: []metav1.ManagedFieldsEntry{ - { - FieldsType: "test", - }, - }, - expected: []metav1.ManagedFieldsEntry{ - { - FieldsType: "FieldsV1", - }, - }, + "invalid fieldsType": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { + managedFields.FieldsType = "invalid fieldsType" + return managedFields, true + }, + "invalid fieldsV1": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { + managedFields.FieldsV1 = &metav1.FieldsV1{Raw: []byte("{invalid}")} + return managedFields, true + }, + "invalid manager": func(managedFields metav1.ManagedFieldsEntry) (metav1.ManagedFieldsEntry, bool) { + managedFields.Manager = "" + return managedFields, false }, } - for _, test := range tests { - obj := &unstructured.Unstructured{} - obj.SetManagedFields(test.beforeAdmission) - wrap.admit = replaceManagedFields(test.afterAdmission) + for name, mutate := range managedFieldsMutators { + t.Run(name, func(t *testing.T) { + mutated, shouldReset := mutate(validManagedFieldsEntry) + validEntries := []metav1.ManagedFieldsEntry{validManagedFieldsEntry} + mutatedEntries := []metav1.ManagedFieldsEntry{mutated} - attrs := admission.NewAttributesRecord(obj, obj, schema.GroupVersionKind{}, "default", "", schema.GroupVersionResource{}, "", admission.Update, nil, false, nil) - if err := ac.(admission.MutationInterface).Admit(context.TODO(), attrs, nil); err != nil { - t.Fatal(err) - } + obj := &v1.ConfigMap{} + obj.SetManagedFields(validEntries) - if !reflect.DeepEqual(obj.GetManagedFields(), test.expected) { - t.Fatalf("expected: \n%v\ngot:\n%v", test.expected, obj.GetManagedFields()) - } + wrap.admit = replaceManagedFields(mutatedEntries) + + attrs := admission.NewAttributesRecord(obj, obj, schema.GroupVersionKind{}, "default", "", schema.GroupVersionResource{}, "", admission.Update, nil, false, nil) + if err := ac.(admission.MutationInterface).Admit(context.TODO(), attrs, nil); err != nil { + t.Fatal(err) + } + + if shouldReset && !reflect.DeepEqual(obj.GetManagedFields(), validEntries) { + t.Fatalf("expected: \n%v\ngot:\n%v", validEntries, obj.GetManagedFields()) + } + if !shouldReset && reflect.DeepEqual(obj.GetManagedFields(), validEntries) { + t.Fatalf("expected: \n%v\ngot:\n%v", mutatedEntries, obj.GetManagedFields()) + } + }) } } From 94149efd8a3b695a6f0945d7b1a8194ffd15f3e9 Mon Sep 17 00:00:00 2001 From: Kevin Wiesmueller Date: Fri, 5 Mar 2021 00:47:13 +0100 Subject: [PATCH 21/21] fix validation test --- .../pkg/apis/meta/v1/validation/validation_test.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go index 33c42357418..4a6f8f30250 100644 --- a/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/apis/meta/v1/validation/validation_test.go @@ -266,11 +266,6 @@ func TestValidateManagedFieldsInvalid(t *testing.T) { Manager: "field\nmanager", APIVersion: "v1", }, - { - Operation: metav1.ManagedFieldsOperationUpdate, - FieldsType: "FieldsV1", - // APIVersion missing - }, } for _, test := range tests {