-
Notifications
You must be signed in to change notification settings - Fork 383
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bug: Patched annotations by admission controller get removed by KCP #3182
Comments
Hi @entigo-mart-erlenheim, thank you for your report! Do you happen to have a minimal implementation of said webhook that you can share with us so we can run it? If not, no problem, we'll be able to test for reproducibility, it would just save us some time. |
@embik Thank you for looking into this problem. I tried putting together a minimal controller that mutates annotations and labels. Unfortunately, I don't currently have time to test this in a KCP cluster, but I validated the functionality in a regular cluster. This controller accepts TLS key and cert with a flag or an environment variable. Optionally, it's possible to create a small nginx proxy to serve the https requests. Inside our KCP deployment we have a centralized workspace for api export where we applied the webhook configuration. Make sure that the server or proxy is running on the port that is specified by the webhook configuration. package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
admission "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/klog/v2"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
"encoding/json"
)
var (
runtimeScheme = runtime.NewScheme()
codecFactory = serializer.NewCodecFactory(runtimeScheme)
deserializer = codecFactory.UniversalDeserializer()
)
// add kind AdmissionReview in scheme
func init() {
_ = corev1.AddToScheme(runtimeScheme)
_ = admission.AddToScheme(runtimeScheme)
}
// serve handles the http portion of a request prior to handing to an admit
// function
func serve(w http.ResponseWriter, r *http.Request) {
var body []byte
if r.Body != nil {
if data, err := io.ReadAll(r.Body); err == nil {
body = data
}
}
contentType := r.Header.Get("Content-Type")
if contentType != "application/json" {
klog.Errorf("contentType=%s, expect application/json", contentType)
return
}
klog.Infof("handling request: %s", body)
var responseObj runtime.Object
if obj, gvk, err := deserializer.Decode(body, nil, nil); err != nil {
msg := fmt.Sprintf("Request could not be decoded: %v", err)
klog.Error(msg)
http.Error(w, msg, http.StatusBadRequest)
return
} else {
requestedAdmissionReview, ok := obj.(*admission.AdmissionReview)
if !ok {
klog.Errorf("Expected v1.AdmissionReview but got: %T", obj)
return
}
object := unstructured.Unstructured{}
if _, _, err := deserializer.Decode(requestedAdmissionReview.Request.Object.Raw, nil, &object); err != nil {
klog.Errorf("Could not decode object: %v", err)
return
}
responseAdmissionReview := &admission.AdmissionReview{}
responseAdmissionReview.SetGroupVersionKind(*gvk)
pt := admission.PatchTypeJSONPatch
patches := getPatches(object)
responseAdmissionReview.Response = &admission.AdmissionResponse{Allowed: true, PatchType: &pt, Patch: patches}
responseAdmissionReview.Response.UID = requestedAdmissionReview.Request.UID
responseObj = responseAdmissionReview
}
klog.Infof("sending response: %v", responseObj)
respBytes, err := json.Marshal(responseObj)
if err != nil {
klog.Error(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
if _, err := w.Write(respBytes); err != nil {
klog.Error(err)
}
}
func getPatches(object unstructured.Unstructured) []byte {
var patches []string
if object.GetLabels() == nil {
patches = append(patches, `{ "op": "add", "path": "/metadata/labels", "value": {} }`)
}
patches = append(patches, `{ "op": "add", "path": "/metadata/labels/foo", "value": "bar" }`)
if object.GetAnnotations() == nil {
patches = append(patches, `{ "op": "add", "path": "/metadata/annotations", "value": {} }`)
}
patches = append(patches, `{ "op": "add", "path": "/metadata/annotations/foo", "value": "bar" }`)
return []byte(fmt.Sprintf(`[%s]`, strings.Join(patches, ",")))
}
func main() {
terminated := make(chan os.Signal, 1)
signal.Notify(terminated, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
tlsKey := os.Getenv("TLS_KEY")
tlsCert := os.Getenv("TLS_CERT")
port := 8081
flag.StringVar(&tlsKey, "tlsKey", tlsKey, "Path to the TLS key")
flag.StringVar(&tlsCert, "tlsCert", tlsCert, "Path to the TLS certificate")
flag.Parse()
http.HandleFunc("/mutate", serve)
http.HandleFunc("/readyz", func(w http.ResponseWriter, req *http.Request) { w.Write([]byte("ok")) })
httpServer := &http.Server{Addr: fmt.Sprintf(":%d", port)}
go func() {
if tlsKey == "" && tlsCert == "" {
klog.Infof("starting server on port %d\n", port)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
klog.Fatalf("Could not listen on %d: %v\n", port, err)
}
} else {
klog.Infof("starting TLS server on port %d\n", port)
if err := httpServer.ListenAndServeTLS(tlsCert, tlsKey); err != nil && !errors.Is(err, http.ErrServerClosed) {
klog.Fatalf("Could not listen on %d: %v\n", port, err)
}
}
}()
<-terminated
stopServer(httpServer)
}
func stopServer(srv *http.Server) {
klog.Info("server shutting down")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
klog.Fatalf("server Shutdown: %+v", err)
}
} Example nginx proxy conf, where conf is mounted to /etc/nginx/nginx.conf and certs to /etc/nginx/certs
Example webhook configuration that calls the controller service for configmaps: apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: development-portal-mutation
webhooks:
- name: "mutateannotation.default.svc"
rules:
- operations: [ "CREATE", "UPDATE" ]
apiGroups: [ "" ]
apiVersions: [ "v1" ]
resources: [ "configmaps" ]
scope: "*"
clientConfig:
service:
namespace: default
name: webhook-server
path: "/mutate"
port: 8443
caBundle: ${ENCODED_CA}
admissionReviewVersions: ["v1"]
sideEffects: None Please let me know if I should provide anything else. |
Describe the bug
We have an admission controller that mutates our custom objects. The admission controller adds 1 annotation and 1 label with JsonPatch. Problem is that the label gets added successfully to the object but the annotation doesn't. I discovered that if I manually add a kcp.io/cluster annotation to the created object then our custom annotation also gets added successfully. I'm suspecting that the process which adds the kcp.io/cluster annotation. Note, annotations that have been manually added to the object aren't affected by this bug.
Steps To Reproduce
Expected Behaviour
Admission controller should be able to successfully add the custom annotation.
Additional Context
I'm not 100% sure that this is caused by KCP but the admission controller does work as expected when used in a local kubernetes cluster without KCP. I have verified that the behaviour described above at least happens in our KCP.
KCP version: v0.26.0
The text was updated successfully, but these errors were encountered: