package core import ( "context" "net/http" "reflect" "github.com/aws/amazon-vpc-resource-controller-k8s/pkg/condition" "github.com/aws/amazon-vpc-resource-controller-k8s/pkg/config" rcHealthz "github.com/aws/amazon-vpc-resource-controller-k8s/pkg/healthz" "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) type NodeUpdateWebhook struct { decoder *admission.Decoder Condition condition.Conditions Log logr.Logger Checker healthz.Checker } func NewNodeUpdateWebhook(condition condition.Conditions, log logr.Logger, healthzHandler *rcHealthz.HealthzHandler) *NodeUpdateWebhook { nodeUpdateWebhook := &NodeUpdateWebhook{ Condition: condition, Log: log, } // add health check on subpath for node validation webhook healthzHandler.AddControllersHealthCheckers( map[string]healthz.Checker{ "health-node-validating-webhook": rcHealthz.SimplePing("node validating webhook", log), }, ) return nodeUpdateWebhook } const awsNodeUsername = "system:serviceaccount:kube-system:aws-node" // +kubebuilder:webhook:path=/validate-v1-node,mutating=false,matchPolicy=Equivalent,failurePolicy=ignore,groups="",resources=nodes,verbs=update,versions=v1,name=vnode.vpc.k8s.aws,sideEffects=None,admissionReviewVersions=v1 // Handle allows update request on Node on the expected fields when the request is // coming from the aws-node Service Account. It also ensures the updates are allowed only // when the Security Group for Pod feature is enabled. func (a *NodeUpdateWebhook) Handle(_ context.Context, req admission.Request) admission.Response { // Allow all requests that are not from aws-node username if req.UserInfo.Username != awsNodeUsername { return admission.Allowed("") } logger := a.Log.WithValues("node", req.Name) logger.Info("update request received from aws-node") newNode := &corev1.Node{} if err := a.decoder.DecodeRaw(req.Object, newNode); err != nil { return admission.Errored(http.StatusBadRequest, err) } oldNode := &corev1.Node{} if err := a.decoder.DecodeRaw(req.OldObject, oldNode); err != nil { return admission.Errored(http.StatusBadRequest, err) } // Remove the values that we expect the aws-node is supposed to modify delete(oldNode.Labels, config.HasTrunkAttachedLabel) delete(newNode.Labels, config.HasTrunkAttachedLabel) delete(oldNode.Labels, config.CustomNetworkingLabel) delete(newNode.Labels, config.CustomNetworkingLabel) // The new object has the ManagedFields which is missing from older object, so remove it as well oldNode.ManagedFields = nil newNode.ManagedFields = nil // Required for v1.18 clusters oldNode.SelfLink = "" newNode.SelfLink = "" // Deny request if there's any modification in the old and new object after removing the fields // added by aws-node if !reflect.DeepEqual(*newNode, *oldNode) { denyMessage := "aws-node can only update limited fields on the Node Object" // Keep log to Debug as it prints entire object logger.V(1).Info("request will be denied", "old object", *oldNode, "new object", *newNode) logger.Info(denyMessage) return admission.Denied(denyMessage) } // If all validation check succeed, allow admission return admission.Allowed("") } // InjectDecoder injects the decoder. func (a *NodeUpdateWebhook) InjectDecoder(d *admission.Decoder) error { a.decoder = d return nil }