From 95242c6159b6d93b96519af9d51c805c796ef6a4 Mon Sep 17 00:00:00 2001 From: Dan Manor Date: Wed, 5 Aug 2026 15:55:31 +0300 Subject: [PATCH 1/3] OSAC-2238: add NetworkClassFetcher interface to pkg/dispatcher Define a NetworkClassFetcher interface and NetworkClassInfo struct that abstract the NetworkClass fetch operation, allowing bare-metal-fulfillment- operator (and other external modules) to import pkg/dispatcher without depending on internal/api/ types. Assisted-by: Claude Code Signed-off-by: Dan Manor --- .../pkg/dispatcher/network_class_fetcher.go | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 osac-operator/pkg/dispatcher/network_class_fetcher.go diff --git a/osac-operator/pkg/dispatcher/network_class_fetcher.go b/osac-operator/pkg/dispatcher/network_class_fetcher.go new file mode 100644 index 000000000..66242aee4 --- /dev/null +++ b/osac-operator/pkg/dispatcher/network_class_fetcher.go @@ -0,0 +1,36 @@ +/* +Copyright 2026. + +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 dispatcher + +import ( + "context" +) + +// NetworkClassInfo holds the manager names extracted from a NetworkClass. +type NetworkClassInfo struct { + // FabricManager is the name of the fabric manager. Always required. + FabricManager string + + // K8sManager is the name of the k8s manager, or empty when the + // NetworkClass does not specify one. + K8sManager string +} + +// NetworkClassFetcher retrieves NetworkClass manager configuration by ID. +type NetworkClassFetcher interface { + FetchNetworkClass(ctx context.Context, id string) (*NetworkClassInfo, error) +} From b5cf8fed0b262aa34b75fad183300e89d381c012 Mon Sep 17 00:00:00 2001 From: Dan Manor Date: Wed, 5 Aug 2026 15:55:42 +0300 Subject: [PATCH 2/3] OSAC-2238: update Resolver to use NetworkClassFetcher interface Replace the privatev1.NetworkClassesClient dependency with the NetworkClassFetcher interface, removing the internal/api/ import from pkg/dispatcher. Update all test stubs to implement the new interface. Assisted-by: Claude Code Signed-off-by: Dan Manor --- osac-operator/pkg/dispatcher/dispatch_test.go | 36 ++++----- osac-operator/pkg/dispatcher/resolver.go | 42 ++++------ .../pkg/dispatcher/resolver_cache_test.go | 37 +++------ osac-operator/pkg/dispatcher/resolver_test.go | 81 ++++++------------- 4 files changed, 70 insertions(+), 126 deletions(-) diff --git a/osac-operator/pkg/dispatcher/dispatch_test.go b/osac-operator/pkg/dispatcher/dispatch_test.go index ee40e6975..7d55ab8ee 100644 --- a/osac-operator/pkg/dispatcher/dispatch_test.go +++ b/osac-operator/pkg/dispatcher/dispatch_test.go @@ -27,10 +27,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - privatev1 "github.com/osac-project/osac/osac-operator/internal/api/osac/private/v1" "github.com/osac-project/osac/osac-operator/pkg/dispatcher" "github.com/osac-project/osac/osac-operator/pkg/networkmanager" - "google.golang.org/grpc" ) var _ = Describe("DispatchTable", func() { @@ -150,22 +148,19 @@ var _ = Describe("Dispatcher", func() { Expect(corev1.AddToScheme(scheme)).To(Succeed()) }) - newStubWithManagers := func(fabricName string, k8sName *string) *stubNetworkClassesClient { - return &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-test", - FabricManager: fabricName, - K8SManager: k8sName, - }, + newStubWithManagers := func(fabricName, k8sName string) *stubNetworkClassFetcher { + return &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { + return &dispatcher.NetworkClassInfo{ + FabricManager: fabricName, + K8sManager: k8sName, }, nil }, } } It("dispatches VirtualNetwork to fabric only", func() { - stub := newStubWithManagers("netris", nil) + stub := newStubWithManagers("netris", "") cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects( newFabricManagerConfigMap("fm-netris", "netris", "ipv4"), ).Build() @@ -183,8 +178,7 @@ var _ = Describe("Dispatcher", func() { }) It("dispatches Subnet to fabric + k8s when both configured", func() { - k8sName := "cudn_localnet" - stub := newStubWithManagers("neutron", &k8sName) + stub := newStubWithManagers("neutron", "cudn_localnet") cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects( newFabricManagerConfigMap("fm-neutron", "neutron", "ipv4,ipv6,dualStack"), newK8sManagerConfigMap("km-cudn", "cudn_localnet", "ipv4,ipv6,dualStack"), @@ -204,8 +198,8 @@ var _ = Describe("Dispatcher", func() { Expect(plan.K8sTarget().Manager.Name).To(Equal("cudn_localnet")) }) - It("dispatches Subnet to fabric only when k8sManager is nil", func() { - stub := newStubWithManagers("netris", nil) + It("dispatches Subnet to fabric only when k8sManager is empty", func() { + stub := newStubWithManagers("netris", "") cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects( newFabricManagerConfigMap("fm-netris", "netris", "ipv4"), ).Build() @@ -223,7 +217,7 @@ var _ = Describe("Dispatcher", func() { }) It("dispatches SecurityGroup to fabric only", func() { - stub := newStubWithManagers("netris", nil) + stub := newStubWithManagers("netris", "") cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects( newFabricManagerConfigMap("fm-netris", "netris", "ipv4"), ).Build() @@ -240,7 +234,7 @@ var _ = Describe("Dispatcher", func() { }) It("dispatches NATGateway to fabric only", func() { - stub := newStubWithManagers("netris", nil) + stub := newStubWithManagers("netris", "") cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects( newFabricManagerConfigMap("fm-netris", "netris", "ipv4"), ).Build() @@ -257,7 +251,7 @@ var _ = Describe("Dispatcher", func() { }) It("returns error for unknown resource kind", func() { - stub := newStubWithManagers("netris", nil) + stub := newStubWithManagers("netris", "") cl := fake.NewClientBuilder().WithScheme(scheme).WithObjects( newFabricManagerConfigMap("fm-netris", "netris", "ipv4"), ).Build() @@ -274,8 +268,8 @@ var _ = Describe("Dispatcher", func() { }) It("propagates resolver errors", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { return nil, fmt.Errorf("connection refused") }, } diff --git a/osac-operator/pkg/dispatcher/resolver.go b/osac-operator/pkg/dispatcher/resolver.go index 392b6fd60..d4255f819 100644 --- a/osac-operator/pkg/dispatcher/resolver.go +++ b/osac-operator/pkg/dispatcher/resolver.go @@ -20,7 +20,6 @@ import ( "context" "fmt" - privatev1 "github.com/osac-project/osac/osac-operator/internal/api/osac/private/v1" "github.com/osac-project/osac/osac-operator/pkg/networkmanager" ) @@ -34,58 +33,51 @@ type ResolvedManagers struct { K8sManager *networkmanager.Manager } -// Resolver fetches a NetworkClass from the fulfillment-service and validates -// its manager references against registered ConfigMaps. +// Resolver fetches a NetworkClass and validates its manager references against +// registered ConfigMaps. type Resolver struct { - networkClassesClient privatev1.NetworkClassesClient - discovery *networkmanager.Discovery + fetcher NetworkClassFetcher + discovery *networkmanager.Discovery } -// NewResolver creates a Resolver that uses the given gRPC client and ConfigMap discovery. +// NewResolver creates a Resolver that uses the given fetcher and ConfigMap discovery. func NewResolver( - ncClient privatev1.NetworkClassesClient, + fetcher NetworkClassFetcher, discovery *networkmanager.Discovery, ) *Resolver { return &Resolver{ - networkClassesClient: ncClient, - discovery: discovery, + fetcher: fetcher, + discovery: discovery, } } -// Resolve fetches the NetworkClass by ID from the fulfillment-service, extracts the -// fabric and k8s manager names, and validates each against the registered ConfigMaps. +// Resolve fetches the NetworkClass by ID, extracts the fabric and k8s manager +// names, and validates each against the registered ConfigMaps. func (r *Resolver) Resolve(ctx context.Context, networkClassID string) (*ResolvedManagers, error) { - resp, err := r.networkClassesClient.Get(ctx, &privatev1.NetworkClassesGetRequest{Id: networkClassID}) + ncInfo, err := r.fetcher.FetchNetworkClass(ctx, networkClassID) if err != nil { return nil, fmt.Errorf("fetching NetworkClass %q: %w", networkClassID, err) } - nc := resp.GetObject() - if nc == nil { - return nil, fmt.Errorf("NetworkClass %q: response contains no object", networkClassID) - } - - fabricManagerName := nc.GetFabricManager() - if fabricManagerName == "" { + if ncInfo.FabricManager == "" { return nil, fmt.Errorf("NetworkClass %q: fabricManager is required but not set", networkClassID) } - fabricMgr, err := r.discovery.GetFabricManager(ctx, fabricManagerName) + fabricMgr, err := r.discovery.GetFabricManager(ctx, ncInfo.FabricManager) if err != nil { return nil, fmt.Errorf("NetworkClass %q: resolving fabricManager %q: %w", - networkClassID, fabricManagerName, err) + networkClassID, ncInfo.FabricManager, err) } result := &ResolvedManagers{ FabricManager: *fabricMgr, } - k8sManagerName := nc.GetK8SManager() - if k8sManagerName != "" { - k8sMgr, err := r.discovery.GetK8sManager(ctx, k8sManagerName) + if ncInfo.K8sManager != "" { + k8sMgr, err := r.discovery.GetK8sManager(ctx, ncInfo.K8sManager) if err != nil { return nil, fmt.Errorf("NetworkClass %q: resolving k8sManager %q: %w", - networkClassID, k8sManagerName, err) + networkClassID, ncInfo.K8sManager, err) } result.K8sManager = k8sMgr } diff --git a/osac-operator/pkg/dispatcher/resolver_cache_test.go b/osac-operator/pkg/dispatcher/resolver_cache_test.go index 7d3e9d16d..268b83ce6 100644 --- a/osac-operator/pkg/dispatcher/resolver_cache_test.go +++ b/osac-operator/pkg/dispatcher/resolver_cache_test.go @@ -27,18 +27,15 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - privatev1 "github.com/osac-project/osac/osac-operator/internal/api/osac/private/v1" "github.com/osac-project/osac/osac-operator/pkg/networkmanager" - "google.golang.org/grpc" ) -type stubNetworkClassesClient struct { - privatev1.NetworkClassesClient - getFunc func(ctx context.Context, in *privatev1.NetworkClassesGetRequest, opts ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) +type stubNetworkClassFetcher struct { + fetchFunc func(ctx context.Context, id string) (*NetworkClassInfo, error) } -func (s *stubNetworkClassesClient) Get(ctx context.Context, in *privatev1.NetworkClassesGetRequest, opts ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return s.getFunc(ctx, in, opts...) +func (s *stubNetworkClassFetcher) FetchNetworkClass(ctx context.Context, id string) (*NetworkClassInfo, error) { + return s.fetchFunc(ctx, id) } func newTestFabricManagerConfigMap(name, managerName, capabilities string) *corev1.ConfigMap { @@ -70,15 +67,10 @@ var _ = Describe("cachedResolver", func() { }) It("caches successful results", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*NetworkClassInfo, error) { callCount++ - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-cached", - FabricManager: "netris", - }, - }, nil + return &NetworkClassInfo{FabricManager: "netris"}, nil }, } @@ -102,8 +94,8 @@ var _ = Describe("cachedResolver", func() { }) It("does not cache errors", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*NetworkClassInfo, error) { callCount++ return nil, fmt.Errorf("unavailable") }, @@ -125,15 +117,10 @@ var _ = Describe("cachedResolver", func() { }) It("caches different NetworkClass IDs independently", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, req *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*NetworkClassInfo, error) { callCount++ - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: req.GetId(), - FabricManager: "netris", - }, - }, nil + return &NetworkClassInfo{FabricManager: "netris"}, nil }, } diff --git a/osac-operator/pkg/dispatcher/resolver_test.go b/osac-operator/pkg/dispatcher/resolver_test.go index c8d31909b..75dd93c8e 100644 --- a/osac-operator/pkg/dispatcher/resolver_test.go +++ b/osac-operator/pkg/dispatcher/resolver_test.go @@ -27,20 +27,16 @@ import ( "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" - privatev1 "github.com/osac-project/osac/osac-operator/internal/api/osac/private/v1" "github.com/osac-project/osac/osac-operator/pkg/dispatcher" "github.com/osac-project/osac/osac-operator/pkg/networkmanager" - "google.golang.org/grpc" ) -// stubNetworkClassesClient implements privatev1.NetworkClassesClient for testing. -type stubNetworkClassesClient struct { - privatev1.NetworkClassesClient - getFunc func(ctx context.Context, in *privatev1.NetworkClassesGetRequest, opts ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) +type stubNetworkClassFetcher struct { + fetchFunc func(ctx context.Context, id string) (*dispatcher.NetworkClassInfo, error) } -func (s *stubNetworkClassesClient) Get(ctx context.Context, in *privatev1.NetworkClassesGetRequest, opts ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return s.getFunc(ctx, in, opts...) +func (s *stubNetworkClassFetcher) FetchNetworkClass(ctx context.Context, id string) (*dispatcher.NetworkClassInfo, error) { + return s.fetchFunc(ctx, id) } func newFabricManagerConfigMap(name, managerName, capabilities string) *corev1.ConfigMap { @@ -84,17 +80,10 @@ var _ = Describe("Resolver", func() { }) It("resolves a fabric-only NetworkClass", func() { - k8sManagerStr := "" - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, req *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - Expect(req.GetId()).To(Equal("nc-1")) - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-1", - FabricManager: "netris", - K8SManager: &k8sManagerStr, - }, - }, nil + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, id string) (*dispatcher.NetworkClassInfo, error) { + Expect(id).To(Equal("nc-1")) + return &dispatcher.NetworkClassInfo{FabricManager: "netris"}, nil }, } @@ -112,15 +101,11 @@ var _ = Describe("Resolver", func() { }) It("resolves a NetworkClass with both fabric and k8s managers", func() { - k8sManagerName := "cudn_localnet" - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-2", - FabricManager: "neutron", - K8SManager: &k8sManagerName, - }, + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { + return &dispatcher.NetworkClassInfo{ + FabricManager: "neutron", + K8sManager: "cudn_localnet", }, nil }, } @@ -143,8 +128,8 @@ var _ = Describe("Resolver", func() { }) It("returns error when NetworkClass is not found", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { return nil, fmt.Errorf("rpc error: code = NotFound") }, } @@ -160,14 +145,9 @@ var _ = Describe("Resolver", func() { }) It("returns error when fabricManager is empty", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-empty", - FabricManager: "", - }, - }, nil + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { + return &dispatcher.NetworkClassInfo{FabricManager: ""}, nil }, } @@ -182,14 +162,9 @@ var _ = Describe("Resolver", func() { }) It("returns error when fabric manager is not registered", func() { - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-bad-fabric", - FabricManager: "unknown-fabric", - }, - }, nil + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { + return &dispatcher.NetworkClassInfo{FabricManager: "unknown-fabric"}, nil }, } @@ -205,15 +180,11 @@ var _ = Describe("Resolver", func() { }) It("returns error when k8s manager is not registered", func() { - k8sManagerName := "missing-k8s" - stub := &stubNetworkClassesClient{ - getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { - return &privatev1.NetworkClassesGetResponse{ - Object: &privatev1.NetworkClass{ - Id: "nc-bad-k8s", - FabricManager: "netris", - K8SManager: &k8sManagerName, - }, + stub := &stubNetworkClassFetcher{ + fetchFunc: func(_ context.Context, _ string) (*dispatcher.NetworkClassInfo, error) { + return &dispatcher.NetworkClassInfo{ + FabricManager: "netris", + K8sManager: "missing-k8s", }, nil }, } From 91bc6b3d51442946ca2db5eff1dce63b276f9e3e Mon Sep 17 00:00:00 2001 From: Dan Manor Date: Wed, 5 Aug 2026 15:56:57 +0300 Subject: [PATCH 3/3] OSAC-2238: add gRPC adapter for NetworkClassFetcher Provide GRPCNetworkClassFetcher in internal/adapters that wraps the generated privatev1.NetworkClassesClient to implement the dispatcher.NetworkClassFetcher interface. This gives osac-operator a drop-in adapter for its existing gRPC client setup. Assisted-by: Claude Code Signed-off-by: Dan Manor --- .../internal/adapters/adapters_suite_test.go | 29 +++++ .../adapters/grpc_network_class_fetcher.go | 60 +++++++++ .../grpc_network_class_fetcher_test.go | 117 ++++++++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 osac-operator/internal/adapters/adapters_suite_test.go create mode 100644 osac-operator/internal/adapters/grpc_network_class_fetcher.go create mode 100644 osac-operator/internal/adapters/grpc_network_class_fetcher_test.go diff --git a/osac-operator/internal/adapters/adapters_suite_test.go b/osac-operator/internal/adapters/adapters_suite_test.go new file mode 100644 index 000000000..1770b9885 --- /dev/null +++ b/osac-operator/internal/adapters/adapters_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2026. + +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 adapters_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAdapters(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Adapters Suite") +} diff --git a/osac-operator/internal/adapters/grpc_network_class_fetcher.go b/osac-operator/internal/adapters/grpc_network_class_fetcher.go new file mode 100644 index 000000000..8e1a22e16 --- /dev/null +++ b/osac-operator/internal/adapters/grpc_network_class_fetcher.go @@ -0,0 +1,60 @@ +/* +Copyright 2026. + +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 adapters provides internal implementations of public pkg/ interfaces +// for osac-operator's own use. +package adapters + +import ( + "context" + "fmt" + + privatev1 "github.com/osac-project/osac/osac-operator/internal/api/osac/private/v1" + "github.com/osac-project/osac/osac-operator/pkg/dispatcher" +) + +// GRPCNetworkClassFetcher adapts the generated privatev1.NetworkClassesClient +// to the dispatcher.NetworkClassFetcher interface. +type GRPCNetworkClassFetcher struct { + client privatev1.NetworkClassesClient +} + +// NewGRPCNetworkClassFetcher creates a fetcher that delegates to the given gRPC client. +func NewGRPCNetworkClassFetcher(client privatev1.NetworkClassesClient) *GRPCNetworkClassFetcher { + return &GRPCNetworkClassFetcher{client: client} +} + +// FetchNetworkClass calls the gRPC NetworkClasses.Get endpoint and returns the +// extracted manager names. +func (f *GRPCNetworkClassFetcher) FetchNetworkClass( + ctx context.Context, + id string, +) (*dispatcher.NetworkClassInfo, error) { + resp, err := f.client.Get(ctx, &privatev1.NetworkClassesGetRequest{Id: id}) + if err != nil { + return nil, err + } + + nc := resp.GetObject() + if nc == nil { + return nil, fmt.Errorf("NetworkClass %q: response contains no object", id) + } + + return &dispatcher.NetworkClassInfo{ + FabricManager: nc.GetFabricManager(), + K8sManager: nc.GetK8SManager(), + }, nil +} diff --git a/osac-operator/internal/adapters/grpc_network_class_fetcher_test.go b/osac-operator/internal/adapters/grpc_network_class_fetcher_test.go new file mode 100644 index 000000000..456aeddb9 --- /dev/null +++ b/osac-operator/internal/adapters/grpc_network_class_fetcher_test.go @@ -0,0 +1,117 @@ +/* +Copyright 2026. + +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 adapters_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "google.golang.org/grpc" + + "github.com/osac-project/osac/osac-operator/internal/adapters" + privatev1 "github.com/osac-project/osac/osac-operator/internal/api/osac/private/v1" +) + +type stubNetworkClassesClient struct { + privatev1.NetworkClassesClient + getFunc func(ctx context.Context, in *privatev1.NetworkClassesGetRequest, opts ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) +} + +func (s *stubNetworkClassesClient) Get( + ctx context.Context, + in *privatev1.NetworkClassesGetRequest, + opts ...grpc.CallOption, +) (*privatev1.NetworkClassesGetResponse, error) { + return s.getFunc(ctx, in, opts...) +} + +var _ = Describe("GRPCNetworkClassFetcher", func() { + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + }) + + It("returns both managers when present", func() { + k8sManager := "cudn_localnet" + stub := &stubNetworkClassesClient{ + getFunc: func(_ context.Context, req *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + Expect(req.GetId()).To(Equal("nc-1")) + return &privatev1.NetworkClassesGetResponse{ + Object: &privatev1.NetworkClass{ + Id: "nc-1", + FabricManager: "netris", + K8SManager: &k8sManager, + }, + }, nil + }, + } + + fetcher := adapters.NewGRPCNetworkClassFetcher(stub) + info, err := fetcher.FetchNetworkClass(ctx, "nc-1") + Expect(err).NotTo(HaveOccurred()) + Expect(info.FabricManager).To(Equal("netris")) + Expect(info.K8sManager).To(Equal("cudn_localnet")) + }) + + It("returns empty K8sManager when not specified", func() { + stub := &stubNetworkClassesClient{ + getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + return &privatev1.NetworkClassesGetResponse{ + Object: &privatev1.NetworkClass{ + Id: "nc-2", + FabricManager: "netris", + }, + }, nil + }, + } + + fetcher := adapters.NewGRPCNetworkClassFetcher(stub) + info, err := fetcher.FetchNetworkClass(ctx, "nc-2") + Expect(err).NotTo(HaveOccurred()) + Expect(info.FabricManager).To(Equal("netris")) + Expect(info.K8sManager).To(BeEmpty()) + }) + + It("propagates gRPC errors", func() { + stub := &stubNetworkClassesClient{ + getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + return nil, fmt.Errorf("rpc error: code = Unavailable") + }, + } + + fetcher := adapters.NewGRPCNetworkClassFetcher(stub) + _, err := fetcher.FetchNetworkClass(ctx, "nc-err") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("Unavailable")) + }) + + It("returns error when response object is nil", func() { + stub := &stubNetworkClassesClient{ + getFunc: func(_ context.Context, _ *privatev1.NetworkClassesGetRequest, _ ...grpc.CallOption) (*privatev1.NetworkClassesGetResponse, error) { + return &privatev1.NetworkClassesGetResponse{Object: nil}, nil + }, + } + + fetcher := adapters.NewGRPCNetworkClassFetcher(stub) + _, err := fetcher.FetchNetworkClass(ctx, "nc-nil") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("response contains no object")) + }) +})