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
3 changes: 0 additions & 3 deletions api/v1alpha1/datasource_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,9 +278,6 @@ type DatasourceList struct {
Items []Datasource `json:"items"`
}

func (*Datasource) URI() string { return "datasources.cortex.cloud/v1alpha1" }
func (*DatasourceList) URI() string { return "datasources.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&Datasource{}, &DatasourceList{})
}
3 changes: 0 additions & 3 deletions api/v1alpha1/decision_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,6 @@ type DecisionList struct {
Items []Decision `json:"items"`
}

func (*Decision) URI() string { return "decisions.cortex.cloud/v1alpha1" }
func (*DecisionList) URI() string { return "decisions.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&Decision{}, &DecisionList{})
}
3 changes: 0 additions & 3 deletions api/v1alpha1/descheduling_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ type DeschedulingList struct {
Items []Descheduling `json:"items"`
}

func (*Descheduling) URI() string { return "deschedulings.cortex.cloud/v1alpha1" }
func (*DeschedulingList) URI() string { return "deschedulings.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&Descheduling{}, &DeschedulingList{})
}
3 changes: 0 additions & 3 deletions api/v1alpha1/knowledge_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,6 @@ type KnowledgeList struct {
Items []Knowledge `json:"items"`
}

func (*Knowledge) URI() string { return "knowledges.cortex.cloud/v1alpha1" }
func (*KnowledgeList) URI() string { return "knowledges.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&Knowledge{}, &KnowledgeList{})
}
3 changes: 0 additions & 3 deletions api/v1alpha1/kpi_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,6 @@ type KPIList struct {
Items []KPI `json:"items"`
}

func (*KPI) URI() string { return "kpis.cortex.cloud/v1alpha1" }
func (*KPIList) URI() string { return "kpis.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&KPI{}, &KPIList{})
}
3 changes: 0 additions & 3 deletions api/v1alpha1/pipeline_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,6 @@ type PipelineList struct {
Items []Pipeline `json:"items"`
}

func (*Pipeline) URI() string { return "pipelines.cortex.cloud/v1alpha1" }
func (*PipelineList) URI() string { return "pipelines.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&Pipeline{}, &PipelineList{})
}
3 changes: 0 additions & 3 deletions api/v1alpha1/reservation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,6 @@ type ReservationList struct {
Items []Reservation `json:"items"`
}

func (*Reservation) URI() string { return "reservations.cortex.cloud/v1alpha1" }
func (*ReservationList) URI() string { return "reservations.cortex.cloud/v1alpha1" }

func init() {
SchemeBuilder.Register(&Reservation{}, &ReservationList{})
}
22 changes: 21 additions & 1 deletion 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 @@ -257,8 +258,27 @@ func main() {
HomeRestConfig: restConfig,
HomeScheme: scheme,
}
// Map the formatted gvk from the config to the actual gvk object so that we
// can look up the right cluster for a given API server override.
var gvksByConfStr = make(map[string]schema.GroupVersionKind)
for gvk := range scheme.AllKnownTypes() {
// This produces something like: "cortex.cloud/v1alpha1/Decision" which can
// be used to look up the right cluster for a given API server override.
formatted := gvk.GroupVersion().String() + "/" + gvk.Kind
gvksByConfStr[formatted] = gvk
}
for gvkStr := range gvksByConfStr {
setupLog.Info("scheme gvk registered", "gvk", gvkStr)
}
for _, override := range config.APIServerOverrides {
cluster, err := multiclusterClient.AddRemote(override.Resource, override.Host, override.CACert)
// Check if we have any registered gvk for this API server override.
gvk, ok := gvksByConfStr[override.GVK]
if !ok {
setupLog.Error(nil, "no registered objects for apiserver override resource",
"apiserver", override.Host, "gvk", override.GVK)
os.Exit(1)
}
cluster, err := multiclusterClient.AddRemote(ctx, override.Host, override.CACert, gvk)
if err != nil {
setupLog.Error(err, "unable to create cluster for apiserver override", "apiserver", override.Host)
os.Exit(1)
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/multicluster/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ tee $TILT_OVERRIDES_PATH <<EOF
global:
conf:
apiServerOverrides:
- resource: decisions.cortex.cloud/v1alpha1
- gvk: cortex.cloud/v1alpha1/Decision
host: https://host.docker.internal:8444
caCert: |
$(cat /tmp/root-ca-remote.pem | sed 's/^/ /')
Expand Down
6 changes: 5 additions & 1 deletion internal/scheduling/explanation/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,11 @@ func (c *Controller) StartupCallback(ctx context.Context) error {
// This function sets up the controller with the provided manager.
func (c *Controller) SetupWithManager(mgr manager.Manager, mcl *multicluster.Client) error {
if !c.SkipIndexFields {
cluster := mcl.ClusterForResource((&v1alpha1.Decision{}).URI())
gvk, err := mcl.GVKFromHomeScheme(&v1alpha1.Decision{})
if err != nil {
return err
}
cluster := mcl.ClusterForResource(gvk)
if err := cluster.GetCache().IndexField(
context.Background(), &v1alpha1.Decision{}, "spec.resourceID",
func(obj client.Object) []string {
Expand Down
4 changes: 2 additions & 2 deletions pkg/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ type DBConfig struct {
// It is assumed that the remote apiserver accepts the serviceaccount tokens
// issued by the local cluster.
type APIServerOverrideConfig struct {
// The resource URI, e.g. "steps.cortex.cloud/v1alpha1"
Resource string `json:"resource"`
// The resource GVK formatted as "<group>/<version>", e.g. "cortex.cloud/v1alpha1/Decision"
GVK string `json:"gvk"`
// The remote kubernetes apiserver url, e.g. "https://my-apiserver:6443"
Host string `json:"host"`
// The root CA certificate to verify the remote apiserver.
Expand Down
12 changes: 7 additions & 5 deletions pkg/multicluster/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ type MulticlusterBuilder struct {
//
// 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.
func (b MulticlusterBuilder) WatchesMulticluster(object client.Object, eventHandler handler.TypedEventHandler[client.Object, reconcile.Request], predicates ...predicate.Predicate) MulticlusterBuilder {
resource, ok := object.(Resource)
if !ok {
b.Builder = b.Watches(object, eventHandler, builder.WithPredicates(predicates...))
return b
cl := b.multiclusterClient.HomeCluster // default cluster
if gvk, err := b.multiclusterClient.GVKFromHomeScheme(object); err == nil {
cl = b.multiclusterClient.ClusterForResource(gvk)
}
cl := b.multiclusterClient.ClusterForResource(resource.URI())
clusterCache := cl.GetCache()
b.Builder = b.WatchesRawSource(source.Kind(clusterCache, object, eventHandler, predicates...))
return b
Expand Down
129 changes: 90 additions & 39 deletions pkg/multicluster/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,107 @@ package multicluster
import (
"testing"

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

type mockResource struct {
client.Object
uri string
// 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),
}

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

func (m *mockResource) URI() string {
return m.uri
// 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
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")
}
}

type mockNonResource struct {
client.Object
// TestMulticlusterBuilder_WatchesMulticluster_RequiresClient tests that
// WatchesMulticluster requires a multicluster client.
func TestMulticlusterBuilder_WatchesMulticluster_RequiresClient(t *testing.T) {
// Create a client with remote clusters
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")
}
}

func TestBuilder_ResourceInterface(t *testing.T) {
// Test that our mock resource implements the Resource interface
var _ Resource = &mockResource{}
// 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
remoteClusters: nil,
}

resource := &mockResource{uri: "test-uri"}
if resource.URI() != "test-uri" {
t.Errorf("Expected URI 'test-uri', got '%s'", resource.URI())
gvk := schema.GroupVersionKind{
Group: "apps",
Version: "v1",
Kind: "Deployment",
}

result := c.ClusterForResource(gvk)
if result != nil {
t.Error("expected nil when HomeCluster is nil")
}
}

func TestResource_TypeAssertion(t *testing.T) {
tests := []struct {
name string
object client.Object
isResource bool
}{
{
name: "resource object",
object: &mockResource{uri: "test-uri"},
isResource: true,
},
{
name: "non-resource object",
object: &mockNonResource{},
isResource: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, ok := tt.object.(Resource)
if ok != tt.isResource {
t.Errorf("Expected isResource=%v, got %v", tt.isResource, ok)
}
})
// 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
c := &Client{
remoteClusters: make(map[schema.GroupVersionKind]cluster.Cluster),
}

gvk := schema.GroupVersionKind{
Group: "apps",
Version: "v1",
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")
}
}
Loading
Loading