diff --git a/k3k-kubelet/controller/syncer/events.go b/k3k-kubelet/controller/syncer/events.go index 0150434b..db1655ed 100644 --- a/k3k-kubelet/controller/syncer/events.go +++ b/k3k-kubelet/controller/syncer/events.go @@ -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) diff --git a/k3k-kubelet/controller/syncer/events_test.go b/k3k-kubelet/controller/syncer/events_test.go index 1c69aa90..ae61bd34 100644 --- a/k3k-kubelet/controller/syncer/events_test.go +++ b/k3k-kubelet/controller/syncer/events_test.go @@ -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()) diff --git a/k3k-kubelet/translate/host.go b/k3k-kubelet/translate/host.go index 1b4f961e..b192ef68 100644 --- a/k3k-kubelet/translate/host.go +++ b/k3k-kubelet/translate/host.go @@ -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 -}