Fix event-mirroring (#1069)

Fix event-mirroring

Lookup the InvolvedObject in the Host cluster and use the annotations on that to get the name of the Pod from the virtual cluster.
This commit is contained in:
Kevin McDermott
2026-07-24 10:45:40 +01:00
committed by GitHub
parent 28bbecb7ee
commit 83c1dd8f59
3 changed files with 87 additions and 143 deletions
+38 -14
View File
@@ -11,6 +11,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/reconcile"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"github.com/rancher/k3k/k3k-kubelet/translate"
@@ -74,31 +75,54 @@ func (s *EventSyncer) Reconcile(ctx context.Context, req reconcile.Request) (rec
return reconcile.Result{}, nil
}
virtualRef := s.Translator.TranslateObjectReferenceFrom(event.InvolvedObject)
// Look up the corresponding object in the virtual cluster.
virtPod := &corev1.Pod{}
if err := s.VirtualClient.Get(ctx, client.ObjectKey{Name: virtualRef.Name, Namespace: virtualRef.Namespace}, virtPod); err != nil {
if client.IgnoreNotFound(err) != nil {
return reconcile.Result{}, fmt.Errorf("could not load virtual object: %w", err)
hostPod := &corev1.Pod{}
if err := s.HostClient.Get(ctx, client.ObjectKey{Name: event.InvolvedObject.Name, Namespace: event.InvolvedObject.Namespace}, hostPod); err != nil {
if apierrors.IsNotFound(err) {
return reconcile.Result{}, fmt.Errorf("could not load host object: %w", err)
}
logger.Info("virtual object not found, skipping event emission",
"virtualInvolvedObject.kind", virtualRef.Kind,
"virtualInvolvedObject.name", virtualRef.Name,
logger.V(1).Info("host object not found, skipping event emission",
"involvedObjectName", event.InvolvedObject.Name,
"involvedObjectNamespace", event.InvolvedObject.Namespace,
)
return reconcile.Result{}, nil
}
logger.V(3).Info("Emitting event into virtual cluster", "virtPod.name",
virtPod.GetName(), "virtPod.namespace", virtPod.GetNamespace(), "reason",
event.Reason, "message", event.Message, "type", event.Type)
s.Translator.TranslateFrom(hostPod)
if hostPod.Name == "" {
logger.V(1).Info("Host object has no name - skipping event",
"involvedObjectName", event.InvolvedObject.Name,
"involvedObjectNamespace", event.InvolvedObject.Namespace,
)
return reconcile.Result{}, nil
}
// Look up the corresponding object in the virtual cluster.
virtPod := &corev1.Pod{}
if err := s.VirtualClient.Get(ctx, client.ObjectKey{Name: hostPod.Name, Namespace: hostPod.Namespace}, virtPod); err != nil {
if client.IgnoreNotFound(err) != nil {
return reconcile.Result{}, fmt.Errorf("could not load virtual object: %w", err)
}
logger.V(1).Info("virtual object not found, skipping event emission",
"virtualInvolvedObjectName", hostPod.Name,
"virtualInvolvedObjectNamespace", hostPod.Namespace,
)
return reconcile.Result{}, nil
}
logger.V(1).Info("Emitting event into virtual cluster",
"virtPodName", virtPod.GetName(), "virtPodNamespace", virtPod.GetNamespace(),
"reason", event.Reason, "message", event.Message, "type", event.Type)
message := translateEventMessage(
event.Message,
event.InvolvedObject.Name, event.InvolvedObject.Namespace,
virtualRef.Name, virtualRef.Namespace,
hostPod.Name, hostPod.Namespace,
)
s.virtEventRecorder.Event(virtPod, event.Type, event.Reason, message)
+49 -82
View File
@@ -7,7 +7,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -24,12 +23,38 @@ func TestEventSyncerReconcile(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
hostPod := func(name, namespace, virtualName, virtualNamespace string) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: map[string]string{
translate.ResourceNameAnnotation: virtualName,
translate.ResourceNamespaceAnnotation: virtualNamespace,
},
UID: types.UID("pod-host-uid"),
ResourceVersion: "1",
},
}
}
virtualPod := func(name, namespace string) *corev1.Pod {
return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
UID: types.UID("pod-virtual-uid"),
ResourceVersion: "1",
},
}
}
tests := map[string]struct {
// the event being reconciled
receivedEvent *corev1.Event
// virtualObj is stored in the virtual fake client, simulating the resource
// in the virtual cluster that the event's InvolvedObject maps to.
virtualObj *unstructured.Unstructured
hostObj *corev1.Pod
virtualObj *corev1.Pod
wantEvent *capturedEvent
}{
"normal event is re-emitted via virtEventRecorder": {
@@ -48,26 +73,10 @@ func TestEventSyncerReconcile(t *testing.T) {
Reason: "Started",
Message: "Container started successfully",
},
virtualObj: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
"resourceVersion": "1",
},
}},
hostObj: hostPod("nginx-test-mycluster-6e67696e782b746573742b6d79636c7573746572", "k3k-mycluster", "nginx", "test"),
virtualObj: virtualPod("nginx", "test"),
wantEvent: &capturedEvent{
Object: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
},
}},
Object: virtualPod("nginx", "test"),
EventType: corev1.EventTypeNormal,
Reason: "Started",
Message: "Container started successfully",
@@ -89,26 +98,10 @@ func TestEventSyncerReconcile(t *testing.T) {
Reason: "BackOff",
Message: "Back-off restarting failed container",
},
virtualObj: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
"resourceVersion": "1",
},
}},
hostObj: hostPod("nginx-test-mycluster-6e67696e782b746573742b6d79636c7573746572", "k3k-mycluster", "nginx", "test"),
virtualObj: virtualPod("nginx", "test"),
wantEvent: &capturedEvent{
Object: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
},
}},
Object: virtualPod("nginx", "test"),
EventType: corev1.EventTypeWarning,
Reason: "BackOff",
Message: "Back-off restarting failed container",
@@ -130,26 +123,10 @@ func TestEventSyncerReconcile(t *testing.T) {
Reason: "",
Message: "some message",
},
virtualObj: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
"resourceVersion": "1",
},
}},
hostObj: hostPod("nginx-test-mycluster-6e67696e782b746573742b6d79636c7573746572", "k3k-mycluster", "nginx", "test"),
virtualObj: virtualPod("nginx", "test"),
wantEvent: &capturedEvent{
Object: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
},
}},
Object: virtualPod("nginx", "test"),
EventType: "",
Reason: "",
Message: "some message",
@@ -171,26 +148,10 @@ func TestEventSyncerReconcile(t *testing.T) {
Reason: "Scheduled",
Message: "Successfully assigned k3k-mycluster/nginx-test-mycluster-6e67696e782b746573742b6d79636c7573746572 to localhost.localdomain",
},
virtualObj: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
"resourceVersion": "1",
},
}},
hostObj: hostPod("nginx-test-mycluster-6e67696e782b746573742b6d79636c7573746572", "k3k-mycluster", "nginx", "test"),
virtualObj: virtualPod("nginx", "test"),
wantEvent: &capturedEvent{
Object: &unstructured.Unstructured{Object: map[string]any{
"apiVersion": "v1",
"kind": "Pod",
"metadata": map[string]any{
"name": "nginx",
"namespace": "test",
"uid": "pod-virtual-uid",
},
}},
Object: virtualPod("nginx", "test"),
EventType: corev1.EventTypeNormal,
Reason: "Scheduled",
Message: "Successfully assigned test/nginx to localhost.localdomain",
@@ -231,6 +192,7 @@ func TestEventSyncerReconcile(t *testing.T) {
Reason: "Started",
Message: "Container started",
},
hostObj: hostPod("some-non-translated-name", "k3k-mycluster", "", ""),
// virtualObj is nil: object not found in virtual cluster → event is skipped
wantEvent: nil,
},
@@ -238,9 +200,14 @@ func TestEventSyncerReconcile(t *testing.T) {
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
hostFakeObjs := []client.Object{tt.receivedEvent}
if tt.hostObj != nil {
hostFakeObjs = append(hostFakeObjs, tt.hostObj)
}
hostFakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(tt.receivedEvent).
WithObjects(hostFakeObjs...).
Build()
virtFakeObjs := []client.Object{}
@@ -287,7 +254,7 @@ func TestEventSyncerReconcile(t *testing.T) {
assert.Equal(t, tt.wantEvent.EventType, got.EventType)
assert.Equal(t, tt.wantEvent.Reason, got.Reason)
assert.Equal(t, tt.wantEvent.Message, got.Message)
wantObj := tt.wantEvent.Object.(*unstructured.Unstructured)
wantObj := tt.wantEvent.Object.(*corev1.Pod)
gotObj := got.Object.(*corev1.Pod)
assert.Equal(t, wantObj.GetName(), gotObj.GetName())
assert.Equal(t, wantObj.GetNamespace(), gotObj.GetNamespace())
-47
View File
@@ -7,8 +7,6 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
corev1 "k8s.io/api/core/v1"
"github.com/rancher/k3k/pkg/apis/k3k.io/v1beta1"
"github.com/rancher/k3k/pkg/controller"
)
@@ -149,48 +147,3 @@ func (t *ToHostTranslator) NamespacedName(obj client.Object) types.NamespacedNam
Name: t.TranslateName(obj.GetNamespace(), obj.GetName()),
}
}
// TranslateObjectReferenceFrom translates a host-cluster ObjectReference back to
// virtual-cluster coordinates by reversing the name encoding applied by TranslateName.
// If the name cannot be reversed (e.g. it was truncated by SafeConcatName), the
// original host name and namespace are preserved.
func (t *ToHostTranslator) TranslateObjectReferenceFrom(ref corev1.ObjectReference) *corev1.ObjectReference {
result := *ref.DeepCopy()
if name, namespace, ok := t.reverseTranslateName(ref.Name); ok {
result.Name = name
result.Namespace = namespace
}
return &result
}
// reverseTranslateName attempts to recover the original virtual name and namespace
// from a host-cluster translated name. TranslateName encodes the original values as
// a hex string suffix (hex("name+namespace+clusterName")), which this method decodes.
// Returns ok=false when the name was truncated and cannot be reversed.
func (t *ToHostTranslator) reverseTranslateName(translatedName string) (name, namespace string, ok bool) {
for i := len(translatedName) - 1; i >= 0; i-- {
if translatedName[i] != '-' {
continue
}
decoded, err := hex.DecodeString(translatedName[i+1:])
if err != nil {
continue
}
parts := strings.SplitN(string(decoded), "+", 3)
switch len(parts) {
case 2:
if parts[1] == t.ClusterName {
return parts[0], "", true
}
case 3:
if parts[2] == t.ClusterName {
return parts[0], parts[1], true
}
}
}
return "", "", false
}