Skip to content
Open
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
5 changes: 5 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
_ "k8s.io/client-go/plugin/pkg/client/auth"

"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
Expand Down Expand Up @@ -264,10 +265,14 @@ func main() {
setupLog.Error(err, "unable to add home cluster")
os.Exit(1)
}
hvGVK := schema.GroupVersionKind{Group: "kvm.cloud.sap", Version: "v1", Kind: "Hypervisor"}
multiclusterClient := &multicluster.Client{
HomeCluster: homeCluster,
HomeRestConfig: restConfig,
HomeScheme: scheme,
ResourceRouters: map[schema.GroupVersionKind]multicluster.ResourceRouter{
hvGVK: multicluster.HypervisorResourceRouter{},
},
}
multiclusterClientConfig := conf.GetConfigOrDie[multicluster.ClientConfig]()
if err := multiclusterClient.InitFromConf(ctx, mgr, multiclusterClientConfig); err != nil {
Expand Down
28 changes: 14 additions & 14 deletions pkg/multicluster/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ type MulticlusterBuilder struct {
multiclusterClient *Client
}

// Watch resources across multiple clusters.
//
// If the object implements Resource, we pick the right cluster based on the
// resource URI. If your builder needs this method, pass it to the builder
// Watch resources, potentially in a remote cluster.
//
// Determines the appropriate cluster by looking up the object's GroupVersionKind (GVK)
// in the home scheme. If your builder needs this method, pass it to the builder
// as the first call and then proceed with other builder methods.
// WatchesMulticluster watches a resource across all clusters that serve its GVK.
// If the GVK is served by multiple remote clusters, a watch is set up on each.
func (b MulticlusterBuilder) WatchesMulticluster(object client.Object, eventHandler handler.TypedEventHandler[client.Object, reconcile.Request], predicates ...predicate.Predicate) MulticlusterBuilder {
cl := b.multiclusterClient.HomeCluster // default cluster
if gvk, err := b.multiclusterClient.GVKFromHomeScheme(object); err == nil {
cl = b.multiclusterClient.ClusterForResource(gvk)
gvk, err := b.multiclusterClient.GVKFromHomeScheme(object)
if err != nil {
// Fall back to home cluster if GVK lookup fails.
clusterCache := b.multiclusterClient.HomeCluster.GetCache()
b.Builder = b.WatchesRawSource(source.Kind(clusterCache, object, eventHandler, predicates...))
return b
}

// Add a watch for each remote cluster serving the GVK
for _, cl := range b.multiclusterClient.ClustersForGVK(gvk) {
clusterCache := cl.GetCache()
b.Builder = b.WatchesRawSource(source.Kind(clusterCache, object, eventHandler, predicates...))
}
clusterCache := cl.GetCache()
b.Builder = b.WatchesRawSource(source.Kind(clusterCache, object, eventHandler, predicates...))
return b
}
73 changes: 17 additions & 56 deletions pkg/multicluster/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,74 +7,34 @@ import (
"testing"

"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/controller-runtime/pkg/cluster"
)

// TestBuildController tests that BuildController creates a MulticlusterBuilder.
// Note: Full integration testing requires a running manager, which is not
// practical for unit tests. This test verifies the basic structure.
func TestBuildController_Structure(t *testing.T) {
// We can't easily test BuildController without a real manager
// because ctrl.NewControllerManagedBy requires a manager implementation.
// Instead, we verify that MulticlusterBuilder has the expected fields.

// Test that MulticlusterBuilder can be created with required fields
c := &Client{
remoteClusters: make(map[schema.GroupVersionKind]cluster.Cluster),
remoteClusters: make(map[schema.GroupVersionKind][]remoteCluster),
}

// Verify the Client field is accessible
if c.remoteClusters == nil {
t.Error("expected remoteClusters to be initialized")
}
}

// TestMulticlusterBuilder_Fields verifies the structure of MulticlusterBuilder.
func TestMulticlusterBuilder_Fields(t *testing.T) {
// Create a minimal client for testing
c := &Client{}

// Create a MulticlusterBuilder manually to test its fields
mb := MulticlusterBuilder{
Builder: nil, // Can't create without manager
Builder: nil,
multiclusterClient: c,
}

// Verify multiclusterClient is set
if mb.multiclusterClient != c {
t.Error("expected multiclusterClient to be set")
}

// Verify Builder can be nil initially
if mb.Builder != nil {
t.Error("expected Builder to be nil when not set")
}
}

// TestMulticlusterBuilder_WatchesMulticluster_RequiresClient tests that
// WatchesMulticluster requires a multicluster client.
func TestMulticlusterBuilder_WatchesMulticluster_RequiresClient(t *testing.T) {
// Create a client with remote clusters
func TestClient_ClustersForGVK_Integration(t *testing.T) {
c := &Client{
remoteClusters: make(map[schema.GroupVersionKind]cluster.Cluster),
}

// Verify the client can be used with the builder
mb := MulticlusterBuilder{
multiclusterClient: c,
}

if mb.multiclusterClient == nil {
t.Error("expected multiclusterClient to be set")
}
}

// TestClient_ClusterForResource_ReturnsHomeCluster tests that ClusterForResource
// returns the home cluster when no remote cluster is configured for the GVK.
func TestClient_ClusterForResource_Integration(t *testing.T) {
// Test with nil remote clusters - should return home cluster
c := &Client{
HomeCluster: nil, // Will return nil
HomeCluster: nil,
remoteClusters: nil,
}

Expand All @@ -84,18 +44,16 @@ func TestClient_ClusterForResource_Integration(t *testing.T) {
Kind: "Deployment",
}

result := c.ClusterForResource(gvk)
if result != nil {
t.Error("expected nil when HomeCluster is nil")
result := c.ClustersForGVK(gvk)
// With nil remoteClusters and nil HomeCluster, returns [nil].
if len(result) != 1 {
t.Errorf("expected 1 cluster, got %d", len(result))
}
}

// TestClient_ClusterForResource_LookupOrder tests the lookup order:
// first check remote clusters, then fall back to home cluster.
func TestClient_ClusterForResource_LookupOrder(t *testing.T) {
// Create client with empty remote clusters map
func TestClient_ClustersForGVK_LookupOrder(t *testing.T) {
c := &Client{
remoteClusters: make(map[schema.GroupVersionKind]cluster.Cluster),
remoteClusters: make(map[schema.GroupVersionKind][]remoteCluster),
}

gvk := schema.GroupVersionKind{
Expand All @@ -104,9 +62,12 @@ func TestClient_ClusterForResource_LookupOrder(t *testing.T) {
Kind: "Deployment",
}

// Should return HomeCluster (nil) since GVK is not in remoteClusters
result := c.ClusterForResource(gvk)
if result != nil {
t.Error("expected nil when GVK not in remoteClusters and HomeCluster is nil")
// No remote clusters for this GVK, returns home cluster (nil).
result := c.ClustersForGVK(gvk)
if len(result) != 1 {
t.Errorf("expected 1 cluster, got %d", len(result))
}
if result[0] != nil {
t.Error("expected nil HomeCluster")
}
}
Loading