Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/klog/v2"

konnectortypes "github.com/kube-bind/kube-bind/pkg/konnector/types"
kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"
)

Expand Down Expand Up @@ -117,6 +118,8 @@ func (r *readReconciler) reconcile(ctx context.Context, providerNamespace, name
logger.Info("Creating missing consumer object", "consumerNamespace", consumerNS, "consumerName", providerObj.GetName())

candidate := candidateFromOwnerObj(consumerNS, providerObj)
konnectortypes.SetSourceMetadataAnnotations(candidate, providerObj.GetNamespace(), string(providerObj.GetUID()),
konnectortypes.ProviderNamespaceAnnotationKey, konnectortypes.ProviderUIDAnnotationKey)
r.makeProviderOwner(candidate)

if _, err := r.createConsumerObject(ctx, candidate); err != nil {
Expand All @@ -132,6 +135,8 @@ func (r *readReconciler) reconcile(ctx context.Context, providerNamespace, name
}

candidate := candidateFromOwnerObj(consumerNS, providerObj)
konnectortypes.SetSourceMetadataAnnotations(candidate, providerObj.GetNamespace(), string(providerObj.GetUID()),
konnectortypes.ProviderNamespaceAnnotationKey, konnectortypes.ProviderUIDAnnotationKey)
current := candidateFromOwnerObj(consumerNS, consumerObj)
if !equality.Semantic.DeepEqual(candidate, current) {
logger.Info("Updating consumer object data", "consumerNamespace", consumerNS, "consumerName", consumerObj.GetName())
Expand Down Expand Up @@ -161,6 +166,8 @@ func (r *readReconciler) reconcile(ctx context.Context, providerNamespace, name
}

candidate := candidateFromOwnerObj(providerNamespace, ownerCandidate)
konnectortypes.SetSourceMetadataAnnotations(candidate, ownerCandidate.GetNamespace(), string(ownerCandidate.GetUID()),
konnectortypes.ConsumerNamespaceAnnotationKey, konnectortypes.ConsumerUIDAnnotationKey)
r.makeConsumerOwner(candidate)

if errors.IsNotFound(providerErr) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Copyright 2026 The Kube Bind Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package claimedresources

import (
"testing"

"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

konnectortypes "github.com/kube-bind/kube-bind/pkg/konnector/types"
)

func TestSetSourceMetadataAnnotations(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
sourceNS string
sourceUID string
nsKey string
uidKey string
expectedAnnotations map[string]string
}{
{
name: "provider annotations on empty object",
obj: &unstructured.Unstructured{},
sourceNS: "provider-ns",
sourceUID: "provider-uid-123",
nsKey: konnectortypes.ProviderNamespaceAnnotationKey,
uidKey: konnectortypes.ProviderUIDAnnotationKey,
expectedAnnotations: map[string]string{
konnectortypes.ProviderNamespaceAnnotationKey: "provider-ns",
konnectortypes.ProviderUIDAnnotationKey: "provider-uid-123",
},
},
{
name: "consumer annotations on empty object",
obj: &unstructured.Unstructured{},
sourceNS: "consumer-ns",
sourceUID: "consumer-uid-456",
nsKey: konnectortypes.ConsumerNamespaceAnnotationKey,
uidKey: konnectortypes.ConsumerUIDAnnotationKey,
expectedAnnotations: map[string]string{
konnectortypes.ConsumerNamespaceAnnotationKey: "consumer-ns",
konnectortypes.ConsumerUIDAnnotationKey: "consumer-uid-456",
},
},
{
name: "preserves existing annotations",
obj: func() *unstructured.Unstructured {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(map[string]string{
"existing-key": "existing-value",
})
return obj
}(),
sourceNS: "my-ns",
sourceUID: "my-uid",
nsKey: konnectortypes.ProviderNamespaceAnnotationKey,
uidKey: konnectortypes.ProviderUIDAnnotationKey,
expectedAnnotations: map[string]string{
konnectortypes.ProviderNamespaceAnnotationKey: "my-ns",
konnectortypes.ProviderUIDAnnotationKey: "my-uid",
"existing-key": "existing-value",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

konnectortypes.SetSourceMetadataAnnotations(tt.obj, tt.sourceNS, tt.sourceUID, tt.nsKey, tt.uidKey)

annotations := tt.obj.GetAnnotations()
for key, expected := range tt.expectedAnnotations {
require.Equal(t, expected, annotations[key], "annotation %s mismatch", key)
}
})
}
}

func TestCandidateFromOwnerObjPreservesKubeBindAnnotations(t *testing.T) {
obj := &unstructured.Unstructured{}
obj.SetAnnotations(map[string]string{
konnectortypes.ProviderNamespaceAnnotationKey: "provider-ns",
konnectortypes.ProviderUIDAnnotationKey: "provider-uid",
konnectortypes.ConsumerNamespaceAnnotationKey: "consumer-ns",
konnectortypes.ConsumerUIDAnnotationKey: "consumer-uid",
"kcp.io/cluster": "should-be-stripped",
"user-annotation": "should-be-kept",
})
obj.SetNamespace("original-ns")
obj.SetName("test-obj")

candidate := candidateFromOwnerObj("target-ns", obj)

annotations := candidate.GetAnnotations()
require.Equal(t, "provider-ns", annotations[konnectortypes.ProviderNamespaceAnnotationKey])
require.Equal(t, "provider-uid", annotations[konnectortypes.ProviderUIDAnnotationKey])
require.Equal(t, "consumer-ns", annotations[konnectortypes.ConsumerNamespaceAnnotationKey])
require.Equal(t, "consumer-uid", annotations[konnectortypes.ConsumerUIDAnnotationKey])
require.Equal(t, "should-be-kept", annotations["user-annotation"])
_, hasKcp := annotations["kcp.io/cluster"]
require.False(t, hasKcp, "kcp.io/cluster annotation should be stripped")
}
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ func (r *reconciler) reconcile(ctx context.Context, obj *unstructured.Unstructur
return err
}

// Annotate the provider-side object with the consumer source metadata.
konnectortypes.SetSourceMetadataAnnotations(upstream, obj.GetNamespace(), string(obj.GetUID()),
konnectortypes.ConsumerNamespaceAnnotationKey, konnectortypes.ConsumerUIDAnnotationKey)

// let the isolation perform any changes it desires
if err := r.isolationStrategy.MutateMetadataAndSpec(upstream, *providerKey); err != nil {
return err
Expand Down Expand Up @@ -147,6 +151,10 @@ func (r *reconciler) reconcile(ctx context.Context, obj *unstructured.Unstructur
return err
}

// (Re)set the consumer source metadata annotations.
konnectortypes.SetSourceMetadataAnnotations(upstream, obj.GetNamespace(), string(obj.GetUID()),
konnectortypes.ConsumerNamespaceAnnotationKey, konnectortypes.ConsumerUIDAnnotationKey)

// just in case, checking for finalizer
if obj, err = r.ensureDownstreamFinalizer(ctx, obj); err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,62 @@ func TestInjectClusterNamespace(t *testing.T) {
}
}

func TestSetSourceAnnotations(t *testing.T) {
tests := []struct {
name string
obj *unstructured.Unstructured
consumerNamespace string
consumerUID string
expectedAnnotations map[string]string
}{
{
name: "no existing annotations",
obj: &unstructured.Unstructured{},
consumerNamespace: "my-namespace",
consumerUID: "abc-123-def",
expectedAnnotations: map[string]string{
konnectortypes.ConsumerNamespaceAnnotationKey: "my-namespace",
konnectortypes.ConsumerUIDAnnotationKey: "abc-123-def",
},
},
{
name: "with existing cluster namespace annotation",
obj: newObjectWithClusterNs("kube-bind-zlp9m"),
consumerNamespace: "other-namespace",
consumerUID: "xyz-456-ghi",
expectedAnnotations: map[string]string{
konnectortypes.ConsumerNamespaceAnnotationKey: "other-namespace",
konnectortypes.ConsumerUIDAnnotationKey: "xyz-456-ghi",
konnectortypes.ClusterNamespaceAnnotationKey: "kube-bind-zlp9m",
},
},
{
name: "cluster-scoped object with empty namespace",
obj: &unstructured.Unstructured{},
consumerNamespace: "",
consumerUID: "uid-789",
expectedAnnotations: map[string]string{
konnectortypes.ConsumerNamespaceAnnotationKey: "",
konnectortypes.ConsumerUIDAnnotationKey: "uid-789",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

konnectortypes.SetSourceMetadataAnnotations(tt.obj, tt.consumerNamespace, tt.consumerUID,
konnectortypes.ConsumerNamespaceAnnotationKey, konnectortypes.ConsumerUIDAnnotationKey)

annotations := tt.obj.GetAnnotations()
for key, expected := range tt.expectedAnnotations {
require.Equal(t, expected, annotations[key], "annotation %s mismatch", key)
}
})
}
}

func newObjectWithClusterNs(providerNamespace string) *unstructured.Unstructured {
obj := &unstructured.Unstructured{}
ans := map[string]string{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ func NewController(

return obj.DeepCopy(), nil
},
updateConsumerObject: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return consumerClient.Resource(gvr).Namespace(obj.GetNamespace()).Update(ctx, obj, metav1.UpdateOptions{})
},
updateConsumerObjectStatus: func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) {
return consumerClient.Resource(gvr).Namespace(obj.GetNamespace()).UpdateStatus(ctx, obj, metav1.UpdateOptions{})
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ import (
"k8s.io/klog/v2"

"github.com/kube-bind/kube-bind/pkg/konnector/controllers/cluster/serviceexport/isolation"
konnectortypes "github.com/kube-bind/kube-bind/pkg/konnector/types"
)

type reconciler struct {
isolationStrategy isolation.Strategy

getConsumerObject func(ns, name string) (*unstructured.Unstructured, error)
updateConsumerObject func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
updateConsumerObjectStatus func(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)

deleteProviderObject func(ctx context.Context, ns, name string) error
Expand Down Expand Up @@ -77,6 +79,20 @@ func (r *reconciler) reconcile(ctx context.Context, obj *unstructured.Unstructur
return err
}

// Set provider source metadata annotations on the consumer object so
// the consumer side can trace where the object came from. This requires
// a full object update because UpdateStatus does not persist metadata.
annotated := downstream.DeepCopy()
konnectortypes.SetSourceMetadataAnnotations(annotated, obj.GetNamespace(), string(obj.GetUID()),
konnectortypes.ProviderNamespaceAnnotationKey, konnectortypes.ProviderUIDAnnotationKey)
if !reflect.DeepEqual(downstream.GetAnnotations(), annotated.GetAnnotations()) {
logger.Info("Updating downstream object provider annotations")
var err error
if downstream, err = r.updateConsumerObject(ctx, annotated); err != nil {
return err
}
}

// let the isolation perform any changes it desires
if err := r.isolationStrategy.MutateStatus(downstream, *consumerKey); err != nil {
return err
Expand Down
32 changes: 32 additions & 0 deletions pkg/konnector/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,40 @@ limitations under the License.

package types

import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)

// ClusterNamespaceAnnotationKey is the annotation key to identify the
// cluster namespace that any synced object on the provider side belongs to.
// This annotation is set on all synced objects, regardless of scope or
// isolation mode.
const ClusterNamespaceAnnotationKey = "kube-bind.io/cluster-namespace"

// ConsumerNamespaceAnnotationKey is the annotation key set on provider-side
// objects to identify the consumer namespace the source object lives in.
const ConsumerNamespaceAnnotationKey = "kube-bind.io/consumer-namespace"

// ConsumerUIDAnnotationKey is the annotation key set on provider-side objects
// to uniquely identify the consumer source object.
const ConsumerUIDAnnotationKey = "kube-bind.io/consumer-uid"

// ProviderNamespaceAnnotationKey is the annotation key set on consumer-side
// objects to identify the provider namespace the source object lives in.
const ProviderNamespaceAnnotationKey = "kube-bind.io/provider-namespace"

// ProviderUIDAnnotationKey is the annotation key set on consumer-side objects
// to uniquely identify the provider source object.
const ProviderUIDAnnotationKey = "kube-bind.io/provider-uid"

// SetSourceMetadataAnnotations sets source metadata annotations on a synced
// object so the receiving side can trace where the object came from.
func SetSourceMetadataAnnotations(obj *unstructured.Unstructured, sourceNS, sourceUID, nsKey, uidKey string) {
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}
annotations[nsKey] = sourceNS
annotations[uidKey] = sourceUID
obj.SetAnnotations(annotations)
}
22 changes: 18 additions & 4 deletions test/e2e/bind/happy-case_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,13 @@ func testHappyCase(
consumer.providerContractNamespace = ourInstance.GetAnnotations()[types.ClusterNamespaceAnnotationKey]
require.NotEmpty(t, consumer.providerContractNamespace, "cluster namespace annotation must always exist")
require.NotEqual(t, consumer.providerContractNamespace, "unknown")

// Verify consumer source metadata annotations are set on the provider-side object.
annotations := ourInstance.GetAnnotations()
Comment thread
mirzakopic marked this conversation as resolved.
_, hasConsumerNS := annotations[types.ConsumerNamespaceAnnotationKey]
require.True(t, hasConsumerNS, "consumer-namespace annotation must exist on provider object")
consumerUID := annotations[types.ConsumerUIDAnnotationKey]
require.NotEmpty(t, consumerUID, "consumer-uid annotation must exist on provider object")
},
},
// Request included namespace, so we check it first
Expand Down Expand Up @@ -673,19 +680,26 @@ func testHappyCase(
})
require.NoError(t, err)

var consumerObj *unstructured.Unstructured
require.Eventually(t, func() bool {
var obj *unstructured.Unstructured
var err error
if consumerResourceScope == apiextensionsv1.NamespaceScoped {
obj, err = consumer.consumerClient.Namespace(consumer.consumerObjectNamespace).Get(ctx, instanceName, metav1.GetOptions{})
consumerObj, err = consumer.consumerClient.Namespace(consumer.consumerObjectNamespace).Get(ctx, instanceName, metav1.GetOptions{})
} else {
obj, err = consumer.consumerClient.Get(ctx, instanceName, metav1.GetOptions{})
consumerObj, err = consumer.consumerClient.Get(ctx, instanceName, metav1.GetOptions{})
}
require.NoError(t, err)
value, _, err := unstructured.NestedString(obj.Object, "status", "result")
value, _, err := unstructured.NestedString(consumerObj.Object, "status", "result")
require.NoError(t, err)
return value == fmt.Sprintf("Ready to ride from %s", consumer.name)
}, wait.ForeverTestTimeout, time.Millisecond*100, "waiting for the %s instance to be updated downstream for %s", serviceGVR.Resource, consumer.name)

// Verify provider source metadata annotations are set on the consumer-side object.
consumerAnnotations := consumerObj.GetAnnotations()
_, hasProviderNS := consumerAnnotations[types.ProviderNamespaceAnnotationKey]
require.True(t, hasProviderNS, "provider-namespace annotation must exist on consumer object")
providerUID := consumerAnnotations[types.ProviderUIDAnnotationKey]
require.NotEmpty(t, providerUID, "provider-uid annotation must exist on consumer object")
},
},

Expand Down
Loading