test managedFields admission validation

This commit is contained in:
Kevin Wiesmueller 2021-01-14 20:14:12 +01:00
parent 5404b5a28a
commit f18d21d6eb
2 changed files with 243 additions and 0 deletions

View File

@ -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",
}},
},
}