From dae45b080702919947bb4635191c04fcf17ad407 Mon Sep 17 00:00:00 2001 From: Tzif Date: Wed, 12 Aug 2026 11:15:10 +0300 Subject: [PATCH] OSAC-3182: stamp GPU fields from InstanceType onto ComputeInstance CR Extend addExplicitFields to read GPU data from the resolved InstanceType and set it on the K8s ComputeInstance spec. Add unit tests for GPU present, GPU absent, and idempotency. Assisted-by: Claude Code Signed-off-by: Tzif --- .../computeinstance_reconciler_function.go | 7 + ...omputeinstance_reconciler_function_test.go | 138 ++++++++++++++++++ 2 files changed, 145 insertions(+) diff --git a/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function.go b/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function.go index 9b1105fbcd..86711fd84b 100644 --- a/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function.go +++ b/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function.go @@ -705,6 +705,13 @@ func (t *task) addExplicitFields(ctx context.Context, spec *osacv1alpha1.Compute itSpec := response.GetObject().GetSpec() spec.Cores = itSpec.GetCores() spec.MemoryGiB = itSpec.GetMemoryGib() + if gpu := itSpec.GetGpu(); gpu != nil { + spec.Gpu = &osacv1alpha1.GpuSpec{ + PciDeviceSelector: gpu.GetPciDeviceSelector(), + ResourceName: gpu.GetResourceName(), + Count: gpu.GetCount(), + } + } if ciSpec.HasRunStrategy() { spec.RunStrategy = osacv1alpha1.RunStrategyType(ciSpec.GetRunStrategy()) } diff --git a/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function_test.go b/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function_test.go index 57791d36ed..fdda1e2f03 100644 --- a/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function_test.go +++ b/fulfillment-service/internal/controllers/computeinstance/computeinstance_reconciler_function_test.go @@ -1909,6 +1909,144 @@ var _ = Describe("instance_type resolution in reconciler", func() { Expect(spec.MemoryGiB).To(Equal(int32(8))) }) + It("resolves instance_type gpu fields onto CR spec", func() { + mockInstanceTypesClient := NewMockInstanceTypesClient(ctrl) + mockInstanceTypesClient.EXPECT(). + Get(gomock.Any(), gomock.Any()). + Return(privatev1.InstanceTypesGetResponse_builder{ + Object: privatev1.InstanceType_builder{ + Id: "gpu-a100-8core", + Spec: privatev1.InstanceTypeSpec_builder{ + Cores: 8, + MemoryGib: 64, + Gpu: privatev1.GpuSpec_builder{ + PciDeviceSelector: "10DE:20B0", + ResourceName: "nvidia.com/A100", + Count: 1, + }.Build(), + }.Build(), + }.Build(), + }.Build(), nil) + + t := &task{ + r: &function{ + logger: logger, + instanceTypesClient: mockInstanceTypesClient, + }, + computeInstance: privatev1.ComputeInstance_builder{ + Id: "test-gpu-instance", + Spec: privatev1.ComputeInstanceSpec_builder{ + Template: &privatev1.ComputeInstanceTemplateReference{Name: "osac.templates.ocp_virt_vm"}, + InstanceType: &privatev1.InstanceTypeReference{Name: "gpu-a100-8core"}, + NetworkAttachments: []*privatev1.NetworkAttachment{ + privatev1.NetworkAttachment_builder{Subnet: &privatev1.SubnetLocalReference{Id: subnetID}}.Build(), + }, + }.Build(), + }.Build(), + hubNamespace: hubNamespace, + hubClient: fakeClient, + } + + spec, err := t.buildSpec(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(spec.Cores).To(Equal(int32(8))) + Expect(spec.MemoryGiB).To(Equal(int32(64))) + Expect(spec.Gpu).ToNot(BeNil()) + Expect(spec.Gpu.PciDeviceSelector).To(Equal("10DE:20B0")) + Expect(spec.Gpu.ResourceName).To(Equal("nvidia.com/A100")) + Expect(spec.Gpu.Count).To(Equal(int32(1))) + }) + + It("leaves gpu nil when InstanceType has no gpu", func() { + mockInstanceTypesClient := NewMockInstanceTypesClient(ctrl) + mockInstanceTypesClient.EXPECT(). + Get(gomock.Any(), gomock.Any()). + Return(privatev1.InstanceTypesGetResponse_builder{ + Object: privatev1.InstanceType_builder{ + Id: "standard-4-8", + Spec: privatev1.InstanceTypeSpec_builder{ + Cores: 4, + MemoryGib: 8, + }.Build(), + }.Build(), + }.Build(), nil) + + t := &task{ + r: &function{ + logger: logger, + instanceTypesClient: mockInstanceTypesClient, + }, + computeInstance: privatev1.ComputeInstance_builder{ + Id: "test-no-gpu-instance", + Spec: privatev1.ComputeInstanceSpec_builder{ + Template: &privatev1.ComputeInstanceTemplateReference{Name: "osac.templates.ocp_virt_vm"}, + InstanceType: &privatev1.InstanceTypeReference{Name: "standard-4-8"}, + NetworkAttachments: []*privatev1.NetworkAttachment{ + privatev1.NetworkAttachment_builder{Subnet: &privatev1.SubnetLocalReference{Id: subnetID}}.Build(), + }, + }.Build(), + }.Build(), + hubNamespace: hubNamespace, + hubClient: fakeClient, + } + + spec, err := t.buildSpec(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(spec.Cores).To(Equal(int32(4))) + Expect(spec.MemoryGiB).To(Equal(int32(8))) + Expect(spec.Gpu).To(BeNil()) + }) + + It("gpu stamping is idempotent", func() { + mockInstanceTypesClient := NewMockInstanceTypesClient(ctrl) + mockInstanceTypesClient.EXPECT(). + Get(gomock.Any(), gomock.Any()). + Return(privatev1.InstanceTypesGetResponse_builder{ + Object: privatev1.InstanceType_builder{ + Id: "gpu-a100-8core", + Spec: privatev1.InstanceTypeSpec_builder{ + Cores: 8, + MemoryGib: 64, + Gpu: privatev1.GpuSpec_builder{ + PciDeviceSelector: "10DE:20B0", + ResourceName: "nvidia.com/A100", + Count: 2, + }.Build(), + }.Build(), + }.Build(), + }.Build(), nil). + Times(2) + + t := &task{ + r: &function{ + logger: logger, + instanceTypesClient: mockInstanceTypesClient, + }, + computeInstance: privatev1.ComputeInstance_builder{ + Id: "test-idempotent-gpu", + Spec: privatev1.ComputeInstanceSpec_builder{ + Template: &privatev1.ComputeInstanceTemplateReference{Name: "osac.templates.ocp_virt_vm"}, + InstanceType: &privatev1.InstanceTypeReference{Name: "gpu-a100-8core"}, + NetworkAttachments: []*privatev1.NetworkAttachment{ + privatev1.NetworkAttachment_builder{Subnet: &privatev1.SubnetLocalReference{Id: subnetID}}.Build(), + }, + }.Build(), + }.Build(), + hubNamespace: hubNamespace, + hubClient: fakeClient, + } + + spec1, err := t.buildSpec(ctx) + Expect(err).ToNot(HaveOccurred()) + + spec2, err := t.buildSpec(ctx) + Expect(err).ToNot(HaveOccurred()) + + Expect(spec1.Gpu).ToNot(BeNil()) + Expect(spec2.Gpu).ToNot(BeNil()) + Expect(*spec1.Gpu).To(Equal(*spec2.Gpu)) + }) + It("returns error when instance_type is empty", func() { t := &task{ r: &function{