k8s.io/kubernetes@v1.29.3/test/integration/apiserver/admissionwebhook/invalid_managedFields_test.go (about)

     1  /*
     2  Copyright 2020 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package admissionwebhook
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"crypto/tls"
    23  	"crypto/x509"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"net/http/httptest"
    29  	"strings"
    30  	"testing"
    31  	"time"
    32  
    33  	v1 "k8s.io/api/admission/v1"
    34  	admissionv1 "k8s.io/api/admissionregistration/v1"
    35  	corev1 "k8s.io/api/core/v1"
    36  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    37  	v1validation "k8s.io/apimachinery/pkg/apis/meta/v1/validation"
    38  	"k8s.io/apimachinery/pkg/types"
    39  	"k8s.io/apimachinery/pkg/util/managedfields"
    40  	"k8s.io/apimachinery/pkg/util/validation/field"
    41  	"k8s.io/apimachinery/pkg/util/wait"
    42  	"k8s.io/apiserver/pkg/endpoints/handlers/fieldmanager"
    43  	clientset "k8s.io/client-go/kubernetes"
    44  	restclient "k8s.io/client-go/rest"
    45  	kubeapiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
    46  	"k8s.io/kubernetes/test/integration/framework"
    47  )
    48  
    49  // TestMutatingWebhookResetsInvalidManagedFields ensures that the API server
    50  // resets managedFields to their state before admission if a mutating webhook
    51  // patches create/update requests with invalid managedFields.
    52  func TestMutatingWebhookResetsInvalidManagedFields(t *testing.T) {
    53  	roots := x509.NewCertPool()
    54  	if !roots.AppendCertsFromPEM(localhostCert) {
    55  		t.Fatal("Failed to append Cert from PEM")
    56  	}
    57  	cert, err := tls.X509KeyPair(localhostCert, localhostKey)
    58  	if err != nil {
    59  		t.Fatalf("Failed to build cert with error: %+v", err)
    60  	}
    61  
    62  	webhookServer := httptest.NewUnstartedServer(newInvalidManagedFieldsWebhookHandler(t))
    63  	webhookServer.TLS = &tls.Config{
    64  		RootCAs:      roots,
    65  		Certificates: []tls.Certificate{cert},
    66  	}
    67  	webhookServer.StartTLS()
    68  	defer webhookServer.Close()
    69  
    70  	s := kubeapiservertesting.StartTestServerOrDie(t,
    71  		kubeapiservertesting.NewDefaultTestServerOptions(), []string{
    72  			"--disable-admission-plugins=ServiceAccount",
    73  		}, framework.SharedEtcd())
    74  	defer s.TearDownFn()
    75  
    76  	recordedWarnings := &bytes.Buffer{}
    77  	warningWriter := restclient.NewWarningWriter(recordedWarnings, restclient.WarningWriterOptions{})
    78  	s.ClientConfig.WarningHandler = warningWriter
    79  	client := clientset.NewForConfigOrDie(s.ClientConfig)
    80  
    81  	if _, err := client.CoreV1().Pods("default").Create(
    82  		context.TODO(), invalidManagedFieldsMarkerFixture, metav1.CreateOptions{}); err != nil {
    83  		t.Fatal(err)
    84  	}
    85  	// make sure we delete the pod even on a failed test
    86  	defer func() {
    87  		if err := client.CoreV1().Pods("default").Delete(context.TODO(), invalidManagedFieldsMarkerFixture.Name, metav1.DeleteOptions{}); err != nil {
    88  			t.Fatalf("failed to delete marker pod: %v", err)
    89  		}
    90  	}()
    91  
    92  	fail := admissionv1.Fail
    93  	none := admissionv1.SideEffectClassNone
    94  	mutatingCfg, err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), &admissionv1.MutatingWebhookConfiguration{
    95  		ObjectMeta: metav1.ObjectMeta{Name: "invalid-managedfields.admission.integration.test"},
    96  		Webhooks: []admissionv1.MutatingWebhook{{
    97  			Name: "invalid-managedfields.admission.integration.test",
    98  			ClientConfig: admissionv1.WebhookClientConfig{
    99  				URL:      &webhookServer.URL,
   100  				CABundle: localhostCert,
   101  			},
   102  			Rules: []admissionv1.RuleWithOperations{{
   103  				Operations: []admissionv1.OperationType{admissionv1.Create, admissionv1.Update},
   104  				Rule:       admissionv1.Rule{APIGroups: []string{""}, APIVersions: []string{"v1"}, Resources: []string{"pods"}},
   105  			}},
   106  			FailurePolicy:           &fail,
   107  			AdmissionReviewVersions: []string{"v1", "v1beta1"},
   108  			SideEffects:             &none,
   109  		}},
   110  	}, metav1.CreateOptions{})
   111  	if err != nil {
   112  		t.Fatal(err)
   113  	}
   114  	defer func() {
   115  		err := client.AdmissionregistrationV1().MutatingWebhookConfigurations().Delete(context.TODO(), mutatingCfg.GetName(), metav1.DeleteOptions{})
   116  		if err != nil {
   117  			t.Fatal(err)
   118  		}
   119  	}()
   120  
   121  	var pod *corev1.Pod
   122  	var lastErr error
   123  	// We just expect the general warning text as the detail order might change
   124  	expectedWarning := fmt.Sprintf(fieldmanager.InvalidManagedFieldsAfterMutatingAdmissionWarningFormat, "")
   125  
   126  	// Make sure reset happens on patch requests
   127  	// wait until new webhook is called
   128  	if err := wait.PollImmediate(time.Millisecond*5, wait.ForeverTestTimeout, func() (bool, error) {
   129  		defer recordedWarnings.Reset()
   130  		pod, err = client.CoreV1().Pods("default").Patch(context.TODO(), invalidManagedFieldsMarkerFixture.Name, types.JSONPatchType, []byte("[]"), metav1.PatchOptions{})
   131  		if err != nil {
   132  			return false, err
   133  		}
   134  		if err := validateManagedFieldsAndDecode(pod.ManagedFields); err != nil {
   135  			lastErr = err
   136  			return false, nil
   137  		}
   138  		if warningWriter.WarningCount() != 1 {
   139  			lastErr = fmt.Errorf("expected one warning, got: %v", warningWriter.WarningCount())
   140  			return false, nil
   141  		}
   142  		if !strings.Contains(recordedWarnings.String(), expectedWarning) {
   143  			lastErr = fmt.Errorf("unexpected warning, expected: \n%v\n, got: \n%v",
   144  				expectedWarning, recordedWarnings.String())
   145  			return false, nil
   146  		}
   147  		lastErr = nil
   148  		return true, nil
   149  	}); err != nil || lastErr != nil {
   150  		t.Fatalf("failed to wait for apiserver handling webhook mutation: %v, last error: %v", err, lastErr)
   151  	}
   152  
   153  	// Make sure reset happens in update requests
   154  	pod, err = client.CoreV1().Pods("default").Update(context.TODO(), pod, metav1.UpdateOptions{})
   155  	if err != nil {
   156  		t.Fatal(err)
   157  	}
   158  	if err := validateManagedFieldsAndDecode(pod.ManagedFields); err != nil {
   159  		t.Error(err)
   160  	}
   161  	if warningWriter.WarningCount() != 2 {
   162  		t.Errorf("expected two warnings, got: %v", warningWriter.WarningCount())
   163  	}
   164  	if !strings.Contains(recordedWarnings.String(), expectedWarning) {
   165  		t.Errorf("unexpected warning, expected: \n%v\n, got: \n%v",
   166  			expectedWarning, recordedWarnings.String())
   167  	}
   168  }
   169  
   170  // validate against both decoding and validation to make sure we use the hardest rule between the both to reset
   171  // with decoding being as strict as it gets, only using it should be enough in admission
   172  func validateManagedFieldsAndDecode(managedFields []metav1.ManagedFieldsEntry) error {
   173  	if err := managedfields.ValidateManagedFields(managedFields); err != nil {
   174  		return err
   175  
   176  	}
   177  	validationErrs := v1validation.ValidateManagedFields(managedFields, field.NewPath("metadata").Child("managedFields"))
   178  	return validationErrs.ToAggregate()
   179  }
   180  
   181  func newInvalidManagedFieldsWebhookHandler(t *testing.T) http.Handler {
   182  	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   183  		defer r.Body.Close()
   184  		data, err := io.ReadAll(r.Body)
   185  		if err != nil {
   186  			http.Error(w, err.Error(), http.StatusBadRequest)
   187  		}
   188  		review := v1.AdmissionReview{}
   189  		if err := json.Unmarshal(data, &review); err != nil {
   190  			http.Error(w, err.Error(), http.StatusBadRequest)
   191  		}
   192  
   193  		if len(review.Request.Object.Raw) == 0 {
   194  			http.Error(w, err.Error(), http.StatusBadRequest)
   195  			return
   196  		}
   197  		pod := &corev1.Pod{}
   198  		if err := json.Unmarshal(review.Request.Object.Raw, pod); err != nil {
   199  			http.Error(w, err.Error(), http.StatusBadRequest)
   200  			return
   201  		}
   202  
   203  		review.Response = &v1.AdmissionResponse{
   204  			Allowed: true,
   205  			UID:     review.Request.UID,
   206  			Result:  &metav1.Status{Message: "admitted"},
   207  		}
   208  
   209  		if len(pod.ManagedFields) != 0 {
   210  			t.Logf("corrupting managedFields %v", pod.ManagedFields)
   211  			review.Response.Patch = []byte(`[
   212  				{"op":"remove","path":"/metadata/managedFields/0/apiVersion"},
   213  				{"op":"remove","path":"/metadata/managedFields/0/fieldsV1"},
   214  				{"op":"remove","path":"/metadata/managedFields/0/fieldsType"}
   215  			]`)
   216  			jsonPatch := v1.PatchTypeJSONPatch
   217  			review.Response.PatchType = &jsonPatch
   218  		}
   219  
   220  		w.Header().Set("Content-Type", "application/json")
   221  		if err := json.NewEncoder(w).Encode(review); err != nil {
   222  			t.Errorf("Marshal of response failed with error: %v", err)
   223  		}
   224  	})
   225  }
   226  
   227  var invalidManagedFieldsMarkerFixture = &corev1.Pod{
   228  	ObjectMeta: metav1.ObjectMeta{
   229  		Namespace: "default",
   230  		Name:      "invalid-managedfields-test-marker",
   231  	},
   232  	Spec: corev1.PodSpec{
   233  		Containers: []corev1.Container{{
   234  			Name:  "fake-name",
   235  			Image: "fakeimage",
   236  		}},
   237  	},
   238  }