Repository: rh-ecosystem-edge/nvidia-ci
Pull request #182: Allow applying a JSON patch to default ClusterPolicy
Author: login: empovit
Labels: name: approved
name: lgtm
Created: 2025-05-21T14:23:53Z | Merged: 2025-05-22T09:14:40Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for applying a JSON patch to the default NVIDIA GPU Operator cluster policy using a new optional environment variable.
- **Documentation**
  - Updated README to document the new environment variable and corrected minor formatting inconsistencies.
- **Bug Fixes**
  - Improved handling and extraction of ALM example JSON objects for more robust cluster policy customization.
- **Chores**
  - Updated dependencies for improved compatibility and error handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/README.md b/README.md
index 40ee969a8..05ab7aca7 100644
--- a/README.md
+++ b/README.md
@@ -78,6 +78,8 @@ NVIDIA GPU Operator-specific parameters for the script are controlled by the fol
 - `NVIDIAGPU_SUBSCRIPTION_UPGRADE_TO_CHANNEL`: specific subscription channel to upgrade to from previous version.  _required when running operator-upgrade testcase_
 - `NVIDIAGPU_CLEANUP`: boolean flag to cleanup up resources created by testcase after testcase execution - Default value is true - _required only when cleanup is not needed_
 - `NVIDIAGPU_GPU_FALLBACK_CATALOGSOURCE_INDEX_IMAGE`: custom certified-operators catalogsource index image for GPU package - _required when deploying fallback custom GPU catalogsource_
+- `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to
+   [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) - _optional_
 - `NFD_FALLBACK_CATALOGSOURCE_INDEX_IMAGE`:  custom redhat-operators catalogsource index image for NFD package - _required when deploying fallback custom NFD catalogsource_
 
 NVIDIA Network Operator-specific (NNO) parameters for the script are controlled by the following environment variables:
@@ -89,18 +91,18 @@ NVIDIA Network Operator-specific (NNO) parameters for the script are controlled
 - `NVIDIANETWORK_CLEANUP`: boolean flag to cleanup up resources created by testcase after testcase execution - Default value is true - _required only when cleanup is not needed_
 - `NVIDIANETWORK_NNO_FALLBACK_CATALOGSOURCE_INDEX_IMAGE`: custom certified-operators catalogsource index image for GPU package - _required when deploying fallback custom NNO catalogsource_
 - `NFD_FALLBACK_CATALOGSOURCE_INDEX_IMAGE`:  custom redhat-operators catalogsource index image for NFD package - _required when deploying fallback custom NFD catalogsource_
-- `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version.  If not specified, the default driver version is used - _optional_    
-- `NVIDIANETWORK_OFED_REPOSITORY`:  OFED Driver Repository.   If not specified, the default repository is used - _optional_            
+- `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version.  If not specified, the default driver version is used - _optional_
+- `NVIDIANETWORK_OFED_REPOSITORY`:  OFED Driver Repository.   If not specified, the default repository is used - _optional_
 - `NVIDIANETWORK_RDMA_WORKLOAD_NAMESPACE`:  RDMA workload pod namespace - _required_
 - `NVIDIANETWORK_RDMA_LINK_TYPE` Layer 2 link type, Infinband or Ethernet - _required_
-- `NVIDIANETWORK_RDMA_MLX_DEVICE:` mlx5 device ID corresponding to the interface port connected to Spectrum or Infiniband switch - _required_
-- `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME:` RDMA Client hostname of first worker node for ib_write_bw test - _required when running the RDMA testcase_ 
-- `NVIDIANETWORK_RDMA_SERVER_HOSTNAME:` RDMA Server hostname of second worker node for ib_write_bw test - _required when running the RDMA testcase_   
-- `NVIDIANETWORK_RDMA_TEST_IMAGE:` RDMA Test Container Image that runs the entrypoint.sh script with optional arguments specified in the pod spec.  This container will clone the "https://github.com/linux-rdma/perftest" repo and builds the ib_write_bw binaries with or without cuda headers.  It will also run the ib_write_bw command with arguments either in CLient or Server mode.  Defaults to "quay.io/wabouham/ecosys-nvidia/rdma-tools:0.0.2" - _optional_
-- `NVIDIANETWORK_MELLANOX_ETH_INTERFACE_NAME`: Mellanox Ethernet Interface Name - Defaults to "ens8f0np0" if not specified - _optional_  
-- `NVIDIANETWORK_MELLANOX_IB_INTERFACE_NAME`:  Mellanox Infiniband Interface Name - Defaults to "ens8f0np0" if not specified - _optional_   
-- `NVIDIANETWORK_MACVLANNETWORK_NAME`: MacvlanNetwork Custom Resource instance name  - Defaults to name from Cluster Service Version alm-examples section if not specified  - _optional_ 
-- `NVIDIANETWORK_MACVLANNETWORK_IPAM_RANGE`: MacvlanNetwork Custom Resource instance IPAM or IP Address/Subnet mask range for Eth or IB interface - _required_    
+- `NVIDIANETWORK_RDMA_MLX_DEVICE`: mlx5 device ID corresponding to the interface port connected to Spectrum or Infiniband switch - _required_
+- `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME`: RDMA Client hostname of first worker node for ib_write_bw test - _required when running the RDMA testcase_
+- `NVIDIANETWORK_RDMA_SERVER_HOSTNAME`: RDMA Server hostname of second worker node for ib_write_bw test - _required when running the RDMA testcase_
+- `NVIDIANETWORK_RDMA_TEST_IMAGE`: RDMA Test Container Image that runs the entrypoint.sh script with optional arguments specified in the pod spec.  This container will clone the "https://github.com/linux-rdma/perftest" repo and builds the ib_write_bw binaries with or without cuda headers.  It will also run the ib_write_bw command with arguments either in CLient or Server mode.  Defaults to "quay.io/wabouham/ecosys-nvidia/rdma-tools:0.0.2" - _optional_
+- `NVIDIANETWORK_MELLANOX_ETH_INTERFACE_NAME`: Mellanox Ethernet Interface Name - Defaults to "ens8f0np0" if not specified - _optional_
+- `NVIDIANETWORK_MELLANOX_IB_INTERFACE_NAME`:  Mellanox Infiniband Interface Name - Defaults to "ens8f0np0" if not specified - _optional_
+- `NVIDIANETWORK_MACVLANNETWORK_NAME`: MacvlanNetwork Custom Resource instance name  - Defaults to name from Cluster Service Version alm-examples section if not specified  - _optional_
+- `NVIDIANETWORK_MACVLANNETWORK_IPAM_RANGE`: MacvlanNetwork Custom Resource instance IPAM or IP Address/Subnet mask range for Eth or IB interface - _required_
 - `NVIDIANETWORK_MACVLANNETWORK_IPAM_GATEWAY`: MacvlanNetwork Custom Resource instance IPAM Default Gateway for specified ip address range - _required_
 
 ### Testing MPS with GPU Operator
diff --git a/go.mod b/go.mod
index c9dc53a95..43cf2aed3 100644
--- a/go.mod
+++ b/go.mod
@@ -6,6 +6,7 @@ require (
 	github.com/Mellanox/network-operator v1.4.0
 	github.com/NVIDIA/gpu-operator v1.8.3-0.20240924212236-e4f1f5d26c11
 	github.com/NVIDIA/k8s-operator-libs v0.0.0-20240826221728-249ba446fa35
+	github.com/evanphx/json-patch/v5 v5.9.11
 	github.com/golang/glog v1.2.4
 	github.com/kelseyhightower/envconfig v1.4.0
 	github.com/onsi/ginkgo/v2 v2.22.2
@@ -39,7 +40,6 @@ require (
 	github.com/containernetworking/cni v1.2.3 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/emicklei/go-restful/v3 v3.12.1 // indirect
-	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
 	github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/go-errors/errors v1.5.1 // indirect
diff --git a/go.sum b/go.sum
index 1b6f2bc2e..b175b1c6b 100644
--- a/go.sum
+++ b/go.sum
@@ -103,8 +103,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
 github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
 github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
-github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
-github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
+github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
+github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=
 github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
diff --git a/internal/nvidiagpuconfig/config.go b/internal/nvidiagpuconfig/config.go
index 7c91b5af1..c6eff416b 100644
--- a/internal/nvidiagpuconfig/config.go
+++ b/internal/nvidiagpuconfig/config.go
@@ -15,6 +15,7 @@ type NvidiaGPUConfig struct {
 	BundleImage                        string `envconfig:"NVIDIAGPU_BUNDLE_IMAGE"`
 	OperatorUpgradeToChannel           string `envconfig:"NVIDIAGPU_SUBSCRIPTION_UPGRADE_TO_CHANNEL"`
 	GPUFallbackCatalogsourceIndexImage string `envconfig:"NVIDIAGPU_GPU_FALLBACK_CATALOGSOURCE_INDEX_IMAGE"`
+	ClusterPolicyPatch                 string `envconfig:"NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH"`
 }
 
 // NewNvidiaGPUConfig returns an instance of NvidiaGPUConfig.
diff --git a/pkg/nvidiagpu/clusterpolicy.go b/pkg/nvidiagpu/clusterpolicy.go
index 00f66cf32..0d61bc52a 100644
--- a/pkg/nvidiagpu/clusterpolicy.go
+++ b/pkg/nvidiagpu/clusterpolicy.go
@@ -3,14 +3,17 @@ package nvidiagpu
 import (
 	"context"
 	"fmt"
+	"strings"
 
 	nvidiagpuv1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1"
+	jsonpatch "github.com/evanphx/json-patch/v5"
 	"github.com/golang/glog"
 	"github.com/rh-ecosystem-edge/nvidia-ci/pkg/clients"
 	"github.com/rh-ecosystem-edge/nvidia-ci/pkg/msg"
+	"github.com/rh-ecosystem-edge/nvidia-ci/pkg/olm"
 	k8serrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/json"
+	k8sjson "k8s.io/apimachinery/pkg/util/json"
 	goclient "sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -30,14 +33,49 @@ type Builder struct {
 
 // NewBuilderFromObjectString creates a Builder object from CSV alm-examples.
 func NewBuilderFromObjectString(apiClient *clients.Settings, almExample string) *Builder {
-	glog.V(100).Infof(
-		"Initializing new Builder structure from almExample string")
+	glog.V(100).Infof("Initializing new Builder structure from almExample string")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
+
+	err = k8sjson.Unmarshal(clusterPolicyExample, &clusterPolicy)
+	return newBuilder(apiClient, &clusterPolicy, err)
+}
+
+// NewBuilderFromObjectStringAndPatch creates a Builder object from CSV alm-examples and applies an RFC6902 JSON patch to it.
+func NewBuilderFromObjectStringAndPatch(apiClient *clients.Settings, almExample, patchJSON string) *Builder {
+	glog.V(100).Infof("Initializing new Builder structure from almExample string and a patch JSON")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	if strings.TrimSpace(patchJSON) == "" {
+		err := fmt.Errorf("patch JSON cannot be an empty string")
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
 
-	clusterPolicy, err := getClusterPolicyFromAlmExample(almExample)
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
+
+	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, fmt.Errorf("invalid JSON patch: %w", err))
+	}
+
+	glog.V(100).Infof("Applying patch to the default cluster policy")
+	modifiedExample, err := patch.Apply(clusterPolicyExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
 
+	err = k8sjson.Unmarshal(modifiedExample, &clusterPolicy)
+	return newBuilder(apiClient, &clusterPolicy, err)
+}
+
+func newBuilder(apiClient *clients.Settings, clusterPolicy *nvidiagpuv1.ClusterPolicy, err error) *Builder {
 	glog.V(100).Infof(
-		"Initializing new Builder structure from almExample string with clusterPolicy name: %s",
-		clusterPolicy.Name)
+		"Initializing new Builder structure with clusterPolicy name: %s", clusterPolicy.Name)
 
 	builder := Builder{
 		apiClient:  apiClient,
@@ -204,27 +242,6 @@ func (builder *Builder) Update(force bool) (*Builder, error) {
 	return builder, err
 }
 
-// getClusterPolicyFromAlmExample extracts the ClusterPolicy from the alm-examples block.
-func getClusterPolicyFromAlmExample(almExample string) (*nvidiagpuv1.ClusterPolicy, error) {
-	clusterPolicyList := &nvidiagpuv1.ClusterPolicyList{}
-
-	if almExample == "" {
-		return nil, fmt.Errorf("almExample is an empty string")
-	}
-
-	err := json.Unmarshal([]byte(almExample), &clusterPolicyList.Items)
-
-	if err != nil {
-		return nil, err
-	}
-
-	if len(clusterPolicyList.Items) == 0 {
-		return nil, fmt.Errorf("failed to get alm examples")
-	}
-
-	return &clusterPolicyList.Items[0], nil
-}
-
 // validate will check that the builder and builder definition are properly initialized before
 // accessing any member fields.
 func (builder *Builder) validate() (bool, error) {
diff --git a/pkg/olm/almexamples.go b/pkg/olm/almexamples.go
new file mode 100644
index 000000000..e73c5388f
--- /dev/null
+++ b/pkg/olm/almexamples.go
@@ -0,0 +1,42 @@
+package olm
+
+import (
+	"encoding/json"
+	"fmt"
+
+	k8sjson "k8s.io/apimachinery/pkg/util/json"
+)
+
+// GetALMExampleItem retrieves the ALM example item at the specified index as a raw JSON,
+// which can be later unmarshalled into custom resource objects. This can be useful if
+// the original JSON needs to be modified, e.g. patched.
+func GetALMExampleItem(index int, almExample string) (json.RawMessage, error) {
+	examples, err := GetALMExampleItems(almExample)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get ALM example items: %w", err)
+	}
+
+	if index < 0 || len(examples) < index+1 {
+		return nil, fmt.Errorf("no alm examples item exists at index %d", index)
+	}
+
+	return examples[index], nil
+}
+
+// GetALMExampleItems retrieves a slice of raw JSON entries from an ALM example string.
+// The raw entries can be later unmarshalled into custom resource objects. This can be
+// useful if the original JSON needs to be modified (e.g. patched), or each entry will
+// be unmarshalled into an object of a different type.
+func GetALMExampleItems(almExample string) ([]json.RawMessage, error) {
+	if almExample == "" {
+		return nil, fmt.Errorf("almExample is an empty string")
+	}
+
+	var examples []json.RawMessage
+	err := k8sjson.Unmarshal([]byte(almExample), &examples)
+	if err != nil {
+		return nil, fmt.Errorf("failed to unmarshal ALM examples: %w", err)
+	}
+
+	return examples, nil
+}
diff --git a/tests/nvidiagpu/deploy-gpu-test.go b/tests/nvidiagpu/deploy-gpu-test.go
index f5ad7647b..a85fab797 100644
--- a/tests/nvidiagpu/deploy-gpu-test.go
+++ b/tests/nvidiagpu/deploy-gpu-test.go
@@ -133,6 +133,14 @@ var _ = Describe("GPU", Ordered, Label(tsparams.LabelSuite), func() {
 					"NVIDIAGPU_SUBSCRIPTION_CHANNEL value '%s'", SubscriptionChannel)
 			}
 
+			if nvidiaGPUConfig.ClusterPolicyPatch == "" {
+				glog.V(gpuparams.GpuLogLevel).Infof("env variable NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH" +
+					" is not set, will deploy default ClusterPolicy")
+			} else {
+				glog.V(gpuparams.GpuLogLevel).Infof("ClusterPolicy patch env variable NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH"+
+					" is set to '%s'", nvidiaGPUConfig.ClusterPolicyPatch)
+			}
+
 			cleanupAfterTest = nvidiaGPUConfig.CleanupAfterTest
 
 			if cleanupAfterTest {
@@ -626,8 +634,16 @@ var _ = Describe("GPU", Ordered, Label(tsparams.LabelSuite), func() {
 			glog.V(gpuparams.GpuLogLevel).Infof("almExamples block from clusterCSV  is : %v ", almExamples)
 
 			By("Deploy ClusterPolicy")
-			glog.V(gpuparams.GpuLogLevel).Infof("Creating ClusterPolicy from CSV almExamples")
-			clusterPolicyBuilder := nvidiagpu.NewBuilderFromObjectString(inittools.APIClient, almExamples)
+
+			var clusterPolicyBuilder *nvidiagpu.Builder
+			if nvidiaGPUConfig.ClusterPolicyPatch == "" {
+				glog.V(gpuparams.GpuLogLevel).Infof("Creating default ClusterPolicy from CSV almExamples without modifications")
+				clusterPolicyBuilder = nvidiagpu.NewBuilderFromObjectString(inittools.APIClient, almExamples)
+			} else {
+				glog.V(gpuparams.GpuLogLevel).Infof("Creating default ClusterPolicy from CSV almExamples and aplying a patch")
+				clusterPolicyBuilder = nvidiagpu.NewBuilderFromObjectStringAndPatch(inittools.APIClient, almExamples, nvidiaGPUConfig.ClusterPolicyPatch)
+			}
+
 			createdClusterPolicyBuilder, err := clusterPolicyBuilder.Create()
 			Expect(err).ToNot(HaveOccurred(), "Error Creating ClusterPolicy from csv "+
 				"almExamples  %v ", err)
diff --git a/vendor/github.com/evanphx/json-patch/v5/merge.go b/vendor/github.com/evanphx/json-patch/v5/merge.go
index f79caf313..d60afadcf 100644
--- a/vendor/github.com/evanphx/json-patch/v5/merge.go
+++ b/vendor/github.com/evanphx/json-patch/v5/merge.go
@@ -103,8 +103,8 @@ func pruneAryNulls(ary *partialArray, options *ApplyOptions) *partialArray {
 	return ary
 }
 
-var errBadJSONDoc = fmt.Errorf("Invalid JSON Document")
-var errBadJSONPatch = fmt.Errorf("Invalid JSON Patch")
+var ErrBadJSONDoc = fmt.Errorf("Invalid JSON Document")
+var ErrBadJSONPatch = fmt.Errorf("Invalid JSON Patch")
 var errBadMergeTypes = fmt.Errorf("Mismatched JSON Documents")
 
 // MergeMergePatches merges two merge patches together, such that
@@ -121,11 +121,11 @@ func MergePatch(docData, patchData []byte) ([]byte, error) {
 
 func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 	if !json.Valid(docData) {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	if !json.Valid(patchData) {
-		return nil, errBadJSONPatch
+		return nil, ErrBadJSONPatch
 	}
 
 	options := NewApplyOptions()
@@ -143,7 +143,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 	patchErr := patch.UnmarshalJSON(patchData)
 
 	if isSyntaxError(docErr) {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	if isSyntaxError(patchErr) {
@@ -151,7 +151,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 	}
 
 	if docErr == nil && doc.obj == nil {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	if patchErr == nil && patch.obj == nil {
@@ -175,7 +175,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 				if json.Valid(patchData) {
 					return patchData, nil
 				}
-				return nil, errBadJSONPatch
+				return nil, ErrBadJSONPatch
 			}
 
 			pruneAryNulls(patchAry, options)
@@ -183,7 +183,7 @@ func doMergePatch(docData, patchData []byte, mergeMerge bool) ([]byte, error) {
 			out, patchErr := json.Marshal(patchAry.nodes)
 
 			if patchErr != nil {
-				return nil, errBadJSONPatch
+				return nil, ErrBadJSONPatch
 			}
 
 			return out, nil
@@ -256,12 +256,12 @@ func createObjectMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
 
 	err := unmarshal(originalJSON, &originalDoc)
 	if err != nil {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	err = unmarshal(modifiedJSON, &modifiedDoc)
 	if err != nil {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	dest, err := getDiff(originalDoc, modifiedDoc)
@@ -286,17 +286,17 @@ func createArrayMergePatch(originalJSON, modifiedJSON []byte) ([]byte, error) {
 
 	err := unmarshal(originalJSON, &originalDocs)
 	if err != nil {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	err = unmarshal(modifiedJSON, &modifiedDocs)
 	if err != nil {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	total := len(originalDocs)
 	if len(modifiedDocs) != total {
-		return nil, errBadJSONDoc
+		return nil, ErrBadJSONDoc
 	}
 
 	result := []json.RawMessage{}
diff --git a/vendor/github.com/evanphx/json-patch/v5/patch.go b/vendor/github.com/evanphx/json-patch/v5/patch.go
index 7a7f71c8b..83102e557 100644
--- a/vendor/github.com/evanphx/json-patch/v5/patch.go
+++ b/vendor/github.com/evanphx/json-patch/v5/patch.go
@@ -2,13 +2,13 @@ package jsonpatch
 
 import (
 	"bytes"
+	"errors"
 	"fmt"
 	"strconv"
 	"strings"
 	"unicode"
 
 	"github.com/evanphx/json-patch/v5/internal/json"
-	"github.com/pkg/errors"
 )
 
 const (
@@ -461,7 +461,7 @@ func (o Operation) Path() (string, error) {
 		return op, nil
 	}
 
-	return "unknown", errors.Wrapf(ErrMissing, "operation missing path field")
+	return "unknown", fmt.Errorf("operation missing path field: %w", ErrMissing)
 }
 
 // From reads the "from" field of the Operation.
@@ -478,7 +478,7 @@ func (o Operation) From() (string, error) {
 		return op, nil
 	}
 
-	return "unknown", errors.Wrapf(ErrMissing, "operation, missing from field")
+	return "unknown", fmt.Errorf("operation, missing from field: %w", ErrMissing)
 }
 
 func (o Operation) value() *lazyNode {
@@ -511,7 +511,7 @@ func (o Operation) ValueInterface() (interface{}, error) {
 		return v, nil
 	}
 
-	return nil, errors.Wrapf(ErrMissing, "operation, missing value field")
+	return nil, fmt.Errorf("operation, missing value field: %w", ErrMissing)
 }
 
 func isArray(buf []byte) bool {
@@ -610,7 +610,7 @@ func (d *partialDoc) get(key string, options *ApplyOptions) (*lazyNode, error) {
 
 	v, ok := d.obj[key]
 	if !ok {
-		return v, errors.Wrapf(ErrMissing, "unable to get nonexistent key: %s", key)
+		return v, fmt.Errorf("unable to get nonexistent key: %s: %w", key, ErrMissing)
 	}
 	return v, nil
 }
@@ -625,7 +625,7 @@ func (d *partialDoc) remove(key string, options *ApplyOptions) error {
 		if options.AllowMissingPathOnRemove {
 			return nil
 		}
-		return errors.Wrapf(ErrMissing, "unable to remove nonexistent key: %s", key)
+		return fmt.Errorf("unable to remove nonexistent key: %s: %w", key, ErrMissing)
 	}
 	idx := -1
 	for i, k := range d.keys {
@@ -649,10 +649,10 @@ func (d *partialArray) set(key string, val *lazyNode, options *ApplyOptions) err
 
 	if idx < 0 {
 		if !options.SupportNegativeIndices {
-			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		if idx < -len(d.nodes) {
-			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		idx += len(d.nodes)
 	}
@@ -669,7 +669,7 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err
 
 	idx, err := strconv.Atoi(key)
 	if err != nil {
-		return errors.Wrapf(err, "value was not a proper array index: '%s'", key)
+		return fmt.Errorf("value was not a proper array index: '%s': %w", key, err)
 	}
 
 	sz := len(d.nodes) + 1
@@ -679,15 +679,15 @@ func (d *partialArray) add(key string, val *lazyNode, options *ApplyOptions) err
 	cur := d
 
 	if idx >= len(ary) {
-		return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 	}
 
 	if idx < 0 {
 		if !options.SupportNegativeIndices {
-			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		if idx < -len(ary) {
-			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		idx += len(ary)
 	}
@@ -713,16 +713,16 @@ func (d *partialArray) get(key string, options *ApplyOptions) (*lazyNode, error)
 
 	if idx < 0 {
 		if !options.SupportNegativeIndices {
-			return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return nil, fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		if idx < -len(d.nodes) {
-			return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return nil, fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		idx += len(d.nodes)
 	}
 
 	if idx >= len(d.nodes) {
-		return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		return nil, fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 	}
 
 	return d.nodes[idx], nil
@@ -740,18 +740,18 @@ func (d *partialArray) remove(key string, options *ApplyOptions) error {
 		if options.AllowMissingPathOnRemove {
 			return nil
 		}
-		return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 	}
 
 	if idx < 0 {
 		if !options.SupportNegativeIndices {
-			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		if idx < -len(cur.nodes) {
 			if options.AllowMissingPathOnRemove {
 				return nil
 			}
-			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+			return fmt.Errorf("Unable to access invalid index: %d: %w", idx, ErrInvalidIndex)
 		}
 		idx += len(cur.nodes)
 	}
@@ -768,7 +768,7 @@ func (d *partialArray) remove(key string, options *ApplyOptions) error {
 func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
-		return errors.Wrapf(ErrMissing, "add operation failed to decode path")
+		return fmt.Errorf("add operation failed to decode path: %w", ErrMissing)
 	}
 
 	// special case, adding to empty means replacing the container with the value given
@@ -809,12 +809,12 @@ func (p Patch) add(doc *container, op Operation, options *ApplyOptions) error {
 	con, key := findObject(doc, path, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "add operation does not apply: doc is missing path: \"%s\"", path)
+		return fmt.Errorf("add operation does not apply: doc is missing path: \"%s\": %w", path, ErrMissing)
 	}
 
 	err = con.add(key, op.value(), options)
 	if err != nil {
-		return errors.Wrapf(err, "error in add for path: '%s'", path)
+		return fmt.Errorf("error in add for path: '%s': %w", path, err)
 	}
 
 	return nil
@@ -867,11 +867,11 @@ func ensurePathExists(pd *container, path string, options *ApplyOptions) error {
 				if arrIndex < 0 {
 
 					if !options.SupportNegativeIndices {
-						return errors.Wrapf(ErrInvalidIndex, "Unable to ensure path for invalid index: %d", arrIndex)
+						return fmt.Errorf("Unable to ensure path for invalid index: %d: %w", arrIndex, ErrInvalidIndex)
 					}
 
 					if arrIndex < -1 {
-						return errors.Wrapf(ErrInvalidIndex, "Unable to ensure path for negative index other than -1: %d", arrIndex)
+						return fmt.Errorf("Unable to ensure path for negative index other than -1: %d: %w", arrIndex, ErrInvalidIndex)
 					}
 
 					arrIndex = 0
@@ -918,11 +918,11 @@ func validateOperation(op Operation) error {
 	switch op.Kind() {
 	case "add", "replace":
 		if _, err := op.ValueInterface(); err != nil {
-			return errors.Wrapf(err, "failed to decode 'value'")
+			return fmt.Errorf("failed to decode 'value': %w", err)
 		}
 	case "move", "copy":
 		if _, err := op.From(); err != nil {
-			return errors.Wrapf(err, "failed to decode 'from'")
+			return fmt.Errorf("failed to decode 'from': %w", err)
 		}
 	case "remove", "test":
 	default:
@@ -930,7 +930,7 @@ func validateOperation(op Operation) error {
 	}
 
 	if _, err := op.Path(); err != nil {
-		return errors.Wrapf(err, "failed to decode 'path'")
+		return fmt.Errorf("failed to decode 'path': %w", err)
 	}
 
 	return nil
@@ -941,10 +941,10 @@ func validatePatch(p Patch) error {
 		if err := validateOperation(op); err != nil {
 			opData, infoErr := json.Marshal(op)
 			if infoErr != nil {
-				return errors.Wrapf(err, "invalid operation")
+				return fmt.Errorf("invalid operation: %w", err)
 			}
 
-			return errors.Wrapf(err, "invalid operation %s", opData)
+			return fmt.Errorf("invalid operation %s: %w", opData, err)
 		}
 	}
 
@@ -954,7 +954,7 @@ func validatePatch(p Patch) error {
 func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
-		return errors.Wrapf(ErrMissing, "remove operation failed to decode path")
+		return fmt.Errorf("remove operation failed to decode path: %w", ErrMissing)
 	}
 
 	con, key := findObject(doc, path, options)
@@ -963,12 +963,12 @@ func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error
 		if options.AllowMissingPathOnRemove {
 			return nil
 		}
-		return errors.Wrapf(ErrMissing, "remove operation does not apply: doc is missing path: \"%s\"", path)
+		return fmt.Errorf("remove operation does not apply: doc is missing path: \"%s\": %w", path, ErrMissing)
 	}
 
 	err = con.remove(key, options)
 	if err != nil {
-		return errors.Wrapf(err, "error in remove for path: '%s'", path)
+		return fmt.Errorf("error in remove for path: '%s': %w", path, err)
 	}
 
 	return nil
@@ -977,7 +977,7 @@ func (p Patch) remove(doc *container, op Operation, options *ApplyOptions) error
 func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
-		return errors.Wrapf(err, "replace operation failed to decode path")
+		return fmt.Errorf("replace operation failed to decode path: %w", err)
 	}
 
 	if path == "" {
@@ -986,7 +986,7 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
 		if val.which == eRaw {
 			if !val.tryDoc() {
 				if !val.tryAry() {
-					return errors.Wrapf(err, "replace operation value must be object or array")
+					return fmt.Errorf("replace operation value must be object or array: %w", err)
 				}
 			} else {
 				val.doc.opts = options
@@ -999,7 +999,7 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
 		case eDoc:
 			*doc = val.doc
 		case eRaw:
-			return errors.Wrapf(err, "replace operation hit impossible case")
+			return fmt.Errorf("replace operation hit impossible case: %w", err)
 		}
 
 		return nil
@@ -1008,17 +1008,17 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
 	con, key := findObject(doc, path, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing path: %s", path)
+		return fmt.Errorf("replace operation does not apply: doc is missing path: %s: %w", path, ErrMissing)
 	}
 
 	_, ok := con.get(key, options)
 	if ok != nil {
-		return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing key: %s", path)
+		return fmt.Errorf("replace operation does not apply: doc is missing key: %s: %w", path, ErrMissing)
 	}
 
 	err = con.set(key, op.value(), options)
 	if err != nil {
-		return errors.Wrapf(err, "error in remove for path: '%s'", path)
+		return fmt.Errorf("error in remove for path: '%s': %w", path, err)
 	}
 
 	return nil
@@ -1027,43 +1027,43 @@ func (p Patch) replace(doc *container, op Operation, options *ApplyOptions) erro
 func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error {
 	from, err := op.From()
 	if err != nil {
-		return errors.Wrapf(err, "move operation failed to decode from")
+		return fmt.Errorf("move operation failed to decode from: %w", err)
 	}
 
 	if from == "" {
-		return errors.Wrapf(ErrInvalid, "unable to move entire document to another path")
+		return fmt.Errorf("unable to move entire document to another path: %w", ErrInvalid)
 	}
 
 	con, key := findObject(doc, from, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing from path: %s", from)
+		return fmt.Errorf("move operation does not apply: doc is missing from path: %s: %w", from, ErrMissing)
 	}
 
 	val, err := con.get(key, options)
 	if err != nil {
-		return errors.Wrapf(err, "error in move for path: '%s'", key)
+		return fmt.Errorf("error in move for path: '%s': %w", key, err)
 	}
 
 	err = con.remove(key, options)
 	if err != nil {
-		return errors.Wrapf(err, "error in move for path: '%s'", key)
+		return fmt.Errorf("error in move for path: '%s': %w", key, err)
 	}
 
 	path, err := op.Path()
 	if err != nil {
-		return errors.Wrapf(err, "move operation failed to decode path")
+		return fmt.Errorf("move operation failed to decode path: %w", err)
 	}
 
 	con, key = findObject(doc, path, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing destination path: %s", path)
+		return fmt.Errorf("move operation does not apply: doc is missing destination path: %s: %w", path, ErrMissing)
 	}
 
 	err = con.add(key, val, options)
 	if err != nil {
-		return errors.Wrapf(err, "error in move for path: '%s'", path)
+		return fmt.Errorf("error in move for path: '%s': %w", path, err)
 	}
 
 	return nil
@@ -1072,7 +1072,7 @@ func (p Patch) move(doc *container, op Operation, options *ApplyOptions) error {
 func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
 	path, err := op.Path()
 	if err != nil {
-		return errors.Wrapf(err, "test operation failed to decode path")
+		return fmt.Errorf("test operation failed to decode path: %w", err)
 	}
 
 	if path == "" {
@@ -1091,18 +1091,18 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
 			return nil
 		}
 
-		return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+		return fmt.Errorf("testing value %s failed: %w", path, ErrTestFailed)
 	}
 
 	con, key := findObject(doc, path, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "test operation does not apply: is missing path: %s", path)
+		return fmt.Errorf("test operation does not apply: is missing path: %s: %w", path, ErrMissing)
 	}
 
 	val, err := con.get(key, options)
-	if err != nil && errors.Cause(err) != ErrMissing {
-		return errors.Wrapf(err, "error in test for path: '%s'", path)
+	if err != nil && errors.Unwrap(err) != ErrMissing {
+		return fmt.Errorf("error in test for path: '%s': %w", path, err)
 	}
 
 	ov := op.value()
@@ -1111,49 +1111,49 @@ func (p Patch) test(doc *container, op Operation, options *ApplyOptions) error {
 		if ov.isNull() {
 			return nil
 		}
-		return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+		return fmt.Errorf("testing value %s failed: %w", path, ErrTestFailed)
 	} else if ov.isNull() {
-		return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+		return fmt.Errorf("testing value %s failed: %w", path, ErrTestFailed)
 	}
 
 	if val.equal(op.value()) {
 		return nil
 	}
 
-	return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+	return fmt.Errorf("testing value %s failed: %w", path, ErrTestFailed)
 }
 
 func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, options *ApplyOptions) error {
 	from, err := op.From()
 	if err != nil {
-		return errors.Wrapf(err, "copy operation failed to decode from")
+		return fmt.Errorf("copy operation failed to decode from: %w", err)
 	}
 
 	con, key := findObject(doc, from, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: \"%s\"", from)
+		return fmt.Errorf("copy operation does not apply: doc is missing from path: \"%s\": %w", from, ErrMissing)
 	}
 
 	val, err := con.get(key, options)
 	if err != nil {
-		return errors.Wrapf(err, "error in copy for from: '%s'", from)
+		return fmt.Errorf("error in copy for from: '%s': %w", from, err)
 	}
 
 	path, err := op.Path()
 	if err != nil {
-		return errors.Wrapf(ErrMissing, "copy operation failed to decode path")
+		return fmt.Errorf("copy operation failed to decode path: %w", ErrMissing)
 	}
 
 	con, key = findObject(doc, path, options)
 
 	if con == nil {
-		return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path)
+		return fmt.Errorf("copy operation does not apply: doc is missing destination path: %s: %w", path, ErrMissing)
 	}
 
 	valCopy, sz, err := deepCopy(val, options)
 	if err != nil {
-		return errors.Wrapf(err, "error while performing deep copy")
+		return fmt.Errorf("error while performing deep copy: %w", err)
 	}
 
 	(*accumulatedCopySize) += int64(sz)
@@ -1163,7 +1163,7 @@ func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64, op
 
 	err = con.add(key, valCopy, options)
 	if err != nil {
-		return errors.Wrapf(err, "error while adding value during copy")
+		return fmt.Errorf("error while adding value during copy: %w", err)
 	}
 
 	return nil
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 2082f0c13..ba8d29363 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -53,7 +53,7 @@ github.com/davecgh/go-spew/spew
 ## explicit; go 1.13
 github.com/emicklei/go-restful/v3
 github.com/emicklei/go-restful/v3/log
-# github.com/evanphx/json-patch/v5 v5.9.0
+# github.com/evanphx/json-patch/v5 v5.9.11
 ## explicit; go 1.18
 github.com/evanphx/json-patch/v5
 github.com/evanphx/json-patch/v5/internal/json


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 4**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>internal/nvidiagpuconfig/config.go (1)</summary><blockquote>

`18-18`: **Suggest pre-validating JSON patch string.**  
Consider parsing or validating the `ClusterPolicyPatch` value during `NewNvidiaGPUConfig` initialization to catch malformed JSON patch syntax early, improving error handling and user feedback.

</blockquote></details>
<details>
<summary>pkg/olm/almexamples.go (1)</summary><blockquote>

`36-39`: **Wrap original parsing error for easier debugging**

`k8sjson.Unmarshal` returns rich error information (offset, syntax, …) that is currently discarded.  
Using `%w` preserves the original error and lets calling code inspect or unwrap it.

```diff
-		return nil, err
+		return nil, fmt.Errorf("failed to unmarshal ALM examples: %w", err)
```

</blockquote></details>
<details>
<summary>pkg/nvidiagpu/clusterpolicy.go (1)</summary><blockquote>

`60-64`: **Log patch failures at a higher verbosity for traceability**

Patching can fail for many reasons (bad path, size limits, …).  Capturing the
error in the builder is good, but emitting it at `V(2)` or `V(5)` (instead of
`V(100)`) would make it visible in typical debug runs without flooding normal
logs.

Consider:

```go
glog.V(5).Infof("Cannot apply cluster policy patch: %v", err)
```

</blockquote></details>
<details>
<summary>vendor/github.com/evanphx/json-patch/patch.go (1)</summary><blockquote>

`10-10`: **Stale dependency on `github.com/pkg/errors`**  

The vendored code still pulls in `github.com/pkg/errors`, which is now
effectively superseded by the standard library’s `%w` error-wrapping
since Go 1.13.  If we must vendor this file, consider the small patch
below to avoid an extra (and un-vendored) dependency:

```diff
-import "github.com/pkg/errors"
+import (
+	"errors"
+	"fmt"
+)
...
-	return nil, errors.Wrapf(ErrMissing, "operation missing path field")
+	return nil, fmt.Errorf("%w: operation missing path field", ErrMissing)
```

(Not urgent if you plan to drop this duplicate library, but worth noting.)

</blockquote></details>
<details>
<summary>vendor/github.com/evanphx/json-patch/merge.go (1)</summary><blockquote>

`94-97`: **Error constants duplicated across two modules**  

`ErrBadJSONDoc`, `ErrBadJSONPatch`, and `errBadMergeTypes` are already
declared in the `/v5` copy of this library.  Duplicating the symbols in
a second module path adds to the confusion noted above and makes “blind
aliasing” (`jsonpatch.ErrBadJSONDoc`) ambiguous for callers.

Once the duplicate library is removed, these duplicates disappear as
well.  No action needed if you follow the recommendation in the first
comment; otherwise consider renaming or un-exporting to avoid clashes.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ed88bbfc6043d8720361221fd86a7a5008134014 and efaf59016479d9a0613fef6cf32ef7fe960f8963.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `go.sum` is excluded by `!**/*.sum`

</details>

<details>
<summary>📒 Files selected for processing (15)</summary>

* `README.md` (2 hunks)
* `go.mod` (2 hunks)
* `internal/nvidiagpuconfig/config.go` (1 hunks)
* `pkg/nvidiagpu/clusterpolicy.go` (2 hunks)
* `pkg/olm/almexamples.go` (1 hunks)
* `tests/nvidiagpu/deploy-gpu-test.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/.gitignore` (1 hunks)
* `vendor/github.com/evanphx/json-patch/LICENSE` (1 hunks)
* `vendor/github.com/evanphx/json-patch/README.md` (1 hunks)
* `vendor/github.com/evanphx/json-patch/errors.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/merge.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/patch.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/v5/merge.go` (6 hunks)
* `vendor/github.com/evanphx/json-patch/v5/patch.go` (27 hunks)
* `vendor/modules.txt` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>vendor/github.com/evanphx/json-patch/v5/patch.go (1)</summary><blockquote>

<details>
<summary>vendor/github.com/evanphx/json-patch/patch.go (7)</summary>

* `ErrMissing` (31-31)
* `ErrInvalidIndex` (34-34)
* `SupportNegativeIndices` (23-23)
* `ErrInvalid` (33-33)
* `ErrTestFailed` (30-30)
* `Patch` (48-48)
* `Operation` (45-45)

</details>

</blockquote></details>
<details>
<summary>vendor/github.com/evanphx/json-patch/merge.go (1)</summary><blockquote>

<details>
<summary>vendor/github.com/evanphx/json-patch/v5/merge.go (5)</summary>

* `MergeMergePatches` (113-115)
* `MergePatch` (118-120)
* `ErrBadJSONDoc` (106-106)
* `ErrBadJSONPatch` (107-107)
* `CreateMergePatch` (233-249)

</details>

</blockquote></details>
<details>
<summary>vendor/github.com/evanphx/json-patch/patch.go (2)</summary><blockquote>

<details>
<summary>vendor/github.com/evanphx/json-patch/v5/patch.go (11)</summary>

* `Operation` (56-56)
* `ErrUnknownType` (37-37)
* `ErrInvalid` (38-38)
* `Equal` (1173-1178)
* `ErrMissing` (36-36)
* `SupportNegativeIndices` (24-24)
* `ErrInvalidIndex` (39-39)
* `Patch` (59-59)
* `ErrTestFailed` (35-35)
* `AccumulatedCopySizeLimit` (27-27)
* `DecodePatch` (1181-1199)

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/errors.go (1)</summary>

* `NewAccumulatedCopySizeError` (14-16)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>README.md</summary>

[uncategorized] ~101-~101: Possible missing comma found.
Context: ...l also run the ib_write_bw command with arguments either in CLient or Server mode.  Defau...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/README.md</summary>

[grammar] ~3-~3: Probable usage error. Use “and” after ‘both’.
Context: ...tf.org/html/rfc6902) against documents, as well as for calculating & applying [RFC7396 JSO...

(BOTH_AS_WELL_AS)

---

[style] ~204-~204: ‘in succession’ might be wordy. Consider a shorter alternative.
Context: ...erging each merge patch to the document in succession.   ```go package main  import ( 	"fmt" ...

(EN_WORDINESS_PREMIUM_IN_SUCCESSION)

---

[misspelling] ~303-~303: Although the phrase ‘are welcomed’ is grammatically correct, possibly, the meaning is not correct in this context. Did you mean “welcome”?
Context: ...ane"} ```  # Help It! Contributions are welcomed! Leave [an issue](https://github.com/ev...

(IS_WELCOMED)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>vendor/github.com/evanphx/json-patch/README.md</summary>

75-75: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

77-77: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

81-81: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

82-82: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

83-83: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

85-85: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

86-86: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

87-87: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

88-88: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

90-90: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

92-92: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

93-93: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

95-95: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

96-96: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

119-119: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

121-121: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

125-125: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

126-126: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

127-127: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

128-128: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

129-129: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

131-131: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

132-132: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

133-133: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

134-134: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

136-136: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

137-137: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

138-138: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

139-139: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

141-141: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

142-142: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

166-166: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

168-168: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

172-172: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

173-173: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

174-174: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

175-175: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

176-176: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

177-177: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

178-178: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

179-179: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

180-180: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

182-182: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

183-183: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

184-184: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

186-186: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

187-187: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

188-188: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

211-211: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

213-213: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

217-217: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

219-219: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

220-220: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

222-222: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

223-223: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

224-224: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

225-225: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

226-226: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

228-228: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

229-229: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

230-230: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

231-231: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

232-232: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

234-234: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

235-235: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

236-236: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

237-237: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

239-239: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

241-241: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

242-242: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

243-243: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

244-244: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

246-246: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

247-247: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

248-248: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

249-249: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

251-251: Hard tabs
Column: 1

(MD010, no-hard-tabs)

---

269-269: Emphasis used instead of a heading
null

(MD036, no-emphasis-as-heading)

---

277-277: Emphasis used instead of a heading
null

(MD036, no-emphasis-as-heading)

---

285-285: Emphasis used instead of a heading
null

(MD036, no-emphasis-as-heading)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (13)</summary><blockquote>

<details>
<summary>go.mod (2)</summary>

`9-9`: **Verify JSON Patch module version consistency.**  
Ensure that the direct dependency `github.com/evanphx/json-patch v5.7.0+incompatible` aligns with the indirect `/v5` module version `v5.9.11`. Consider upgrading the direct requirement to `v5.9.11+incompatible` to avoid mixing patch versions between import paths.

---

`43-43`: **Approve indirect JSON Patch `/v5` version bump.**  
The indirect upgrade from `v5.9.0` to `v5.9.11` ensures the latest patch fixes are included without breaking changes.

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/.gitignore (1)</summary>

`1-6`: **Approve `.gitignore` for vendored JSON Patch directory.**  
This file cleanly excludes IDE/editor artifacts and macOS system files from version control within the `json-patch` vendor directory.

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/LICENSE (1)</summary>

`1-26`: **Approve LICENSE addition for vendored JSON Patch.**  
The BSD-style license file correctly covers the `json-patch` vendor code and grants the necessary permissions under the specified conditions.

</details>
<details>
<summary>vendor/modules.txt (1)</summary>

`56-63`: **Ensure `vendor/modules.txt` was regenerated correctly.**  
Confirm that these entries were generated by running `go mod vendor` (not manually edited) so that vendor metadata stays in sync with `go.mod`.

</details>
<details>
<summary>README.md (1)</summary>

`81-82`: **Documentation looks good.**

Clear and well-referenced documentation for the new environment variable. The RFC link and kubectl patch reference provide users with the necessary context for creating valid JSON patches.

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/v5/patch.go (2)</summary>

`5-5`: **Updated error package import is appropriate.**

Switching from the third-party `github.com/pkg/errors` package to the standard library's `errors` package aligns with modern Go error handling using the `%w` verb in `fmt.Errorf`.

---

`464-464`: **Consistent error wrapping implementation.**

All error wrapping has been updated to use `fmt.Errorf` with `%w` verb instead of using the third-party `github.com/pkg/errors` package. This modernization maintains the same error semantics while simplifying dependencies.



Also applies to: 481-481, 514-514, 613-613, 628-628, 652-652, 655-656, 672-673, 681-682, 687-688, 690-691, 716-717, 719-720, 724-726, 743-744, 748-749, 754-755, 771-772, 811-813, 817-818, 967-967, 1010-1012, 1015-1016, 1021-1022, 1035-1037, 1040-1041, 1045-1046, 1056-1057, 1061-1062, 1066-1067, 1075-1076, 1094-1095, 1101-1101, 1104-1106, 1114-1115, 1116-1117, 1123-1124, 1129-1130, 1135-1136, 1140-1141, 1145-1146, 1151-1152, 1156-1157, 1166-1167

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/v5/merge.go (2)</summary>

`106-107`: **Appropriately exported error variables.**

Exporting the error variables to `ErrBadJSONDoc` and `ErrBadJSONPatch` is a good practice that allows consumers of this library to check for specific error types using `errors.Is()`.

---

`124-125`: **Consistent error variable reference updates.**

All references to the renamed error variables have been properly updated throughout the file, maintaining consistent behavior.



Also applies to: 128-129, 146-147, 153-155, 178-179, 186-187, 259-260, 264-265, 289-290, 294-295, 299-300

</details>
<details>
<summary>tests/nvidiagpu/deploy-gpu-test.go (1)</summary>

`631-636`: **Proper implementation of conditional JSON patching.**

The code appropriately checks for the presence of a patch string and conditionally uses either the original or the new patch-enabled builder method. This maintains backward compatibility while adding the new patching functionality.

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/README.md (1)</summary>

`5-6`: **Vendor files are normally kept verbatim**

Style/grammar issues spotted by the linters (e.g. *“both … as well as”*,
*“Contributions are welcomed”*) exist in upstream text.  Altering vendor
material risks diverging from the upstream checksum and complicating future
`go mod vendor` updates.  It is usually best to leave README files untouched
unless there is a legal/compliance requirement to do so.

</details>
<details>
<summary>vendor/github.com/evanphx/json-patch/errors.go (1)</summary>

`18-21`: **Optional: surface limits in the error type**

`AccumulatedCopySizeError` currently exposes `limit` and `accumulated` as
unexported fields, so callers cannot inspect them after error unwrapping.  
If library API stability allows, consider exporting the fields or adding
getter methods to facilitate programmatic handling.

Purely informational for upstream; no action required in the vendored copy.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:31:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (4)</summary><blockquote>

<details>
<summary>README.md (4)</summary><blockquote>

`81-82`: **Maintain consistent unordered list style**  
The new `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH` entry uses a hyphen (`-`), while the rest of the environment variable list uses asterisks (`*`). Please switch to an asterisk for consistency:

```diff
- - `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to
-    [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) - _optional_
+ * `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to
+   [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) - _optional_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`94-95`: **Use asterisk for list items**  
The entries for `NVIDIANETWORK_OFED_DRIVER_VERSION` and `NVIDIANETWORK_OFED_REPOSITORY` currently use `-`. Please update to `*`:

```diff
- - `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version. If not specified, the default driver version is used - _optional_
- - `NVIDIANETWORK_OFED_REPOSITORY`: OFED Driver Repository. If not specified, the default repository is used - _optional_
+ * `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version. If not specified, the default driver version is used - _optional_
+ * `NVIDIANETWORK_OFED_REPOSITORY`: OFED Driver Repository. If not specified, the default repository is used - _optional_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`99-100`: **Switch to asterisk for RDMA host entries**  
The `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME` and `NVIDIANETWORK_RDMA_SERVER_HOSTNAME` entries should follow the asterisk style:

```diff
- - `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME`: RDMA Client hostname of first worker node for ib_write_bw test - _required when running the RDMA testcase_
- - `NVIDIANETWORK_RDMA_SERVER_HOSTNAME`: RDMA Server hostname of second worker node for ib_write_bw test - _required when running the RDMA testcase_
+ * `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME`: RDMA Client hostname of first worker node for ib_write_bw test - _required when running the RDMA testcase_
+ * `NVIDIANETWORK_RDMA_SERVER_HOSTNAME`: RDMA Server hostname of second worker node for ib_write_bw test - _required when running the RDMA testcase_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`102-105`: **Unify bullet style for new Network Operator variables**  
Please update these entries to use `*` instead of `-`:

```diff
- - `NVIDIANETWORK_MELLANOX_ETH_INTERFACE_NAME`: Mellanox Ethernet Interface Name - Defaults to "ens8f0np0" if not specified - _optional_
- - `NVIDIANETWORK_MELLANOX_IB_INTERFACE_NAME`: Mellanox Infiniband Interface Name - Defaults to "ens8f0np0" if not specified - _optional_
- - `NVIDIANETWORK_MACVLANNETWORK_NAME`: MacvlanNetwork Custom Resource instance name  - Defaults to name from Cluster Service Version alm-examples section if not specified  - _optional_
- - `NVIDIANETWORK_MACVLANNETWORK_IPAM_RANGE`: MacvlanNetwork Custom Resource instance IPAM or IP Address/Subnet mask range for Eth or IB interface - _required_
+ * `NVIDIANETWORK_MELLANOX_ETH_INTERFACE_NAME`: Mellanox Ethernet Interface Name - Defaults to "ens8f0np0" if not specified - _optional_
+ * `NVIDIANETWORK_MELLANOX_IB_INTERFACE_NAME`: Mellanox Infiniband Interface Name - Defaults to "ens8f0np0" if not specified - _optional_
+ * `NVIDIANETWORK_MACVLANNETWORK_NAME`: MacvlanNetwork Custom Resource instance name - Defaults to name from Cluster Service Version alm-examples section if not specified - _optional_
+ * `NVIDIANETWORK_MACVLANNETWORK_IPAM_RANGE`: MacvlanNetwork Custom Resource instance IPAM or IP Address/Subnet mask range for Eth or IB interface - _required_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between efaf59016479d9a0613fef6cf32ef7fe960f8963 and fbb1b41b088efbfe72bba0fb1ed0f6886afdbc02.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `go.sum` is excluded by `!**/*.sum`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `README.md` (2 hunks)
* `go.mod` (1 hunks)
* `internal/nvidiagpuconfig/config.go` (1 hunks)
* `pkg/nvidiagpu/clusterpolicy.go` (2 hunks)
* `pkg/olm/almexamples.go` (1 hunks)
* `tests/nvidiagpu/deploy-gpu-test.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/v5/merge.go` (6 hunks)
* `vendor/github.com/evanphx/json-patch/v5/patch.go` (27 hunks)
* `vendor/modules.txt` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* vendor/modules.txt
* tests/nvidiagpu/deploy-gpu-test.go

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (6)</summary>

* internal/nvidiagpuconfig/config.go
* go.mod
* pkg/olm/almexamples.go
* vendor/github.com/evanphx/json-patch/v5/patch.go
* vendor/github.com/evanphx/json-patch/v5/merge.go
* pkg/nvidiagpu/clusterpolicy.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>README.md</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

96-96: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

97-97: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

98-98: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Bare URL used
null

(MD034, no-bare-urls)

---

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:55:10Z
user: login: empovit
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-21T14:55:32Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T14:56:01Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>README.md (3)</summary><blockquote>

`81-82`: **Add a usage example for the JSON patch environment variable**  
It would be helpful to include a concrete shell snippet showing how to set `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH` with a valid RFC 6902 patch (including quoting/escaping). For example:  
```bash
export NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH='[{"op":"replace","path":"/spec/template/spec/containers/0/imagePullPolicy","value":"Always"}]'
```  
This will guide users on proper formatting.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`94-95`: **Clarify the default values for OFED driver settings**  
The docs mention that a default driver version and repository will be used if these variables are unset, but don’t specify what those defaults are. Please list the actual default version/repository (or reference where users can find them) to improve clarity.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`99-100`: **Ensure consistent Markdown styling for variable names**  
The colon should be outside the backticks to match other entries. For example:
```diff
- `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME:` RDMA Client hostname...
+ `NVIDIANETWORK_RDMA_CLIENT_HOSTNAME`: RDMA Client hostname...
- `NVIDIANETWORK_RDMA_SERVER_HOSTNAME:` RDMA Server hostname...
+ `NVIDIANETWORK_RDMA_SERVER_HOSTNAME`: RDMA Server hostname...
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between fbb1b41b088efbfe72bba0fb1ed0f6886afdbc02 and c4fafbf9da0ad1639dd47e7865ff24dd01e1d20d.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `go.sum` is excluded by `!**/*.sum`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `README.md` (2 hunks)
* `go.mod` (1 hunks)
* `internal/nvidiagpuconfig/config.go` (1 hunks)
* `pkg/nvidiagpu/clusterpolicy.go` (2 hunks)
* `pkg/olm/almexamples.go` (1 hunks)
* `tests/nvidiagpu/deploy-gpu-test.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/v5/merge.go` (6 hunks)
* `vendor/github.com/evanphx/json-patch/v5/patch.go` (27 hunks)
* `vendor/modules.txt` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* vendor/modules.txt
* go.mod
* internal/nvidiagpuconfig/config.go
* vendor/github.com/evanphx/json-patch/v5/merge.go
* vendor/github.com/evanphx/json-patch/v5/patch.go
* tests/nvidiagpu/deploy-gpu-test.go
* pkg/olm/almexamples.go
* pkg/nvidiagpu/clusterpolicy.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>README.md</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

96-96: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

97-97: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

98-98: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Bare URL used
null

(MD034, no-bare-urls)

---

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>README.md</summary>

[uncategorized] ~101-~101: Possible missing comma found.
Context: ...l also run the ib_write_bw command with arguments either in CLient or Server mode.  Defau...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (1)</summary><blockquote>

<details>
<summary>README.md (1)</summary>

`102-105`: **Documentation for Mellanox and MacvlanNetwork variables is clear**  
These entries correctly specify defaults and requirement status for `NVIDIANETWORK_MELLANOX_ETH_INTERFACE_NAME`, `NVIDIANETWORK_MELLANOX_IB_INTERFACE_NAME`, `NVIDIANETWORK_MACVLANNETWORK_NAME`, and `NVIDIANETWORK_MACVLANNETWORK_IPAM_RANGE`.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:11:10Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>pkg/nvidiagpu/clusterpolicy.go (1)</summary><blockquote>

`61-64`: **Enhance error message for invalid JSON patches.**

Current error message when patch decoding fails could be more descriptive to help with troubleshooting.


```diff
-		return newBuilder(apiClient, &clusterPolicy, err)
+		return newBuilder(apiClient, &clusterPolicy, fmt.Errorf("invalid JSON patch: %w", err))
```

</blockquote></details>
<details>
<summary>README.md (2)</summary><blockquote>

`81-82`: **Good documentation for the new environment variable.**

The documentation clearly explains the purpose of the new `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH` environment variable and provides helpful references to RFC 6902 and kubectl patch documentation.



Consider formatting the URL as a proper Markdown link:

```diff
-- `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to
--   [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) - _optional_
+- `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to 
+  [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) - _optional_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`94-105`: **Consistent formatting of Network Operator environment variables section.**

The environment variable descriptions in the NVIDIA Network Operator section have been cleaned up by removing trailing spaces.



For consistency with the rest of the document, consider using asterisks instead of dashes for list items as flagged by markdownlint:

```diff
-- `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version.  If not specified, the default driver version is used - _optional_
+* `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version.  If not specified, the default driver version is used - _optional_
```

And fix the typo in line 101 ("CLient" should be "Client"):

```diff
-- `NVIDIANETWORK_RDMA_TEST_IMAGE`: RDMA Test Container Image that runs the entrypoint.sh script with optional arguments specified in the pod spec.  This container will clone the "https://github.com/linux-rdma/perftest" repo and builds the ib_write_bw binaries with or without cuda headers.  It will also run the ib_write_bw command with arguments either in CLient or Server mode.  Defaults to "quay.io/wabouham/ecosys-nvidia/rdma-tools:0.0.2" - _optional_
+* `NVIDIANETWORK_RDMA_TEST_IMAGE`: RDMA Test Container Image that runs the entrypoint.sh script with optional arguments specified in the pod spec.  This container will clone the "https://github.com/linux-rdma/perftest" repo and builds the ib_write_bw binaries with or without cuda headers.  It will also run the ib_write_bw command with arguments either in Client or Server mode.  Defaults to "quay.io/wabouham/ecosys-nvidia/rdma-tools:0.0.2" - _optional_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~101-~101: Possible missing comma found.
Context: ...l also run the ib_write_bw command with arguments either in CLient or Server mode.  Defau...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

96-96: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

97-97: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

98-98: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Bare URL used
null

(MD034, no-bare-urls)

---

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c4fafbf9da0ad1639dd47e7865ff24dd01e1d20d and ebeb08e5c93b5cf8c2fc217d976e9cbba723f255.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `go.sum` is excluded by `!**/*.sum`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `README.md` (2 hunks)
* `go.mod` (1 hunks)
* `internal/nvidiagpuconfig/config.go` (1 hunks)
* `pkg/nvidiagpu/clusterpolicy.go` (2 hunks)
* `pkg/olm/almexamples.go` (1 hunks)
* `tests/nvidiagpu/deploy-gpu-test.go` (1 hunks)
* `vendor/github.com/evanphx/json-patch/v5/merge.go` (6 hunks)
* `vendor/github.com/evanphx/json-patch/v5/patch.go` (27 hunks)
* `vendor/modules.txt` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (7)</summary>

* go.mod
* internal/nvidiagpuconfig/config.go
* tests/nvidiagpu/deploy-gpu-test.go
* vendor/modules.txt
* vendor/github.com/evanphx/json-patch/v5/patch.go
* pkg/olm/almexamples.go
* vendor/github.com/evanphx/json-patch/v5/merge.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>README.md</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

96-96: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

97-97: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

98-98: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Bare URL used
null

(MD034, no-bare-urls)

---

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>README.md</summary>

[uncategorized] ~101-~101: Possible missing comma found.
Context: ...l also run the ib_write_bw command with arguments either in CLient or Server mode.  Defau...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>pkg/nvidiagpu/clusterpolicy.go (5)</summary>

`6-16`: **Good addition of required imports for the patching feature.**

The imports for `strings`, `jsonpatch`, `olm`, and the `k8sjson` alias are appropriate for implementing the JSON patching functionality.

---

`34-45`: **Clean refactoring of the builder initialization.**

Refactoring to use the new `olm.GetALMExampleItem` function is a good improvement that simplifies the code and leverages the new common functionality for ALM example extraction.

---

`47-54`: **Appropriate implementation of empty patch validation.**

The empty patch check is correctly implemented and follows the strategy discussed in previous review comments - treating an empty patch as an error rather than falling back to the unpatched flow.

---

`67-70`: **Good implementation of patch application with appropriate error handling.**

The patch application logic and error handling are implemented correctly. The log message at line 66 provides good context about what's happening.

---

`76-99`: **Well-designed helper function for builder initialization.**

The `newBuilder` function is a good abstraction that centralizes the common logic for initializing a Builder object, reducing code duplication and ensuring consistent error handling.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T15:37:45Z
user: login: wabouhamad
body: Very clean implementation and very useful capability added !  Just one minor comment about logging the input env var value or mentioning in the log we are using a patched clusterpolicy for better troubleshooting. 
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-21T20:44:22Z
user: login: TomerNewman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-22T06:20:58Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>README.md (2)</summary><blockquote>

`81-82`: **Use consistent unordered list style for environment variables**

The new `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH` entry is using a dash (`-`) as the bullet, whereas the rest of the document uses an asterisk (`*`). Please update it to maintain consistency:

```diff
- - `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to
-   [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) - _optional_
+ * `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH`: a JSON patch to apply to a default cluster policy from ALM examples, written according to
+   [RFC 6902](http://tools.ietf.org/html/rfc6902) (also see [kubectl patch](https://kubernetes.io/docs/reference/kubectl/generated/kubectl_patch/)) _optional_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

---

`94-106`: **Normalize bullets and convert bare URLs to markdown links**

The newly added NVIDIA Network Operator variables also use dash bullets instead of asterisks, and the description for `NVIDIANETWORK_RDMA_TEST_IMAGE` contains a bare URL. To align with existing style and avoid bare URLs, please apply:

```diff
- - `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version.  If not specified, the default driver version is used - _optional_
+ * `NVIDIANETWORK_OFED_DRIVER_VERSION`: OFED Driver Version.  If not specified, the default driver version is used _optional_

...  

- - `NVIDIANETWORK_RDMA_TEST_IMAGE`: RDMA Test Container Image that runs the entrypoint.sh script with optional arguments specified in the pod spec.  This container will clone the "https://github.com/linux-rdma/perftest" repo...
+ * `NVIDIANETWORK_RDMA_TEST_IMAGE`: RDMA Test Container Image that runs the entrypoint.sh script with optional arguments specified in the pod spec.  This container will clone the [linux-rdma/perftest](https://github.com/linux-rdma/perftest) repo...

...  

- - `NVIDIANETWORK_MACVLANNETWORK_IPAM_GATEWAY`: MacvlanNetwork Custom Resource instance IPAM Default Gateway for specified ip address range - _required_
+ * `NVIDIANETWORK_MACVLANNETWORK_IPAM_GATEWAY`: MacvlanNetwork Custom Resource instance IPAM Default Gateway for specified IP address range _required_
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~101-~101: Possible missing comma found.
Context: ...l also run the ib_write_bw command with arguments either in CLient or Server mode.  Defau...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

96-96: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

97-97: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

98-98: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Bare URL used
null

(MD034, no-bare-urls)

---

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

106-106: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**


<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between ebeb08e5c93b5cf8c2fc217d976e9cbba723f255 and e33b4b424d17d1d6a6e6b8a2cd6cdc38da21cebc.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `go.sum` is excluded by `!**/*.sum`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `README.md` (2 hunks)
* `go.mod` (1 hunks)
* `internal/nvidiagpuconfig/config.go` (1 hunks)
* `pkg/nvidiagpu/clusterpolicy.go` (2 hunks)
* `pkg/olm/almexamples.go` (1 hunks)
* `tests/nvidiagpu/deploy-gpu-test.go` (2 hunks)
* `vendor/github.com/evanphx/json-patch/v5/merge.go` (6 hunks)
* `vendor/github.com/evanphx/json-patch/v5/patch.go` (27 hunks)
* `vendor/modules.txt` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (8)</summary>

* internal/nvidiagpuconfig/config.go
* vendor/modules.txt
* go.mod
* vendor/github.com/evanphx/json-patch/v5/patch.go
* vendor/github.com/evanphx/json-patch/v5/merge.go
* tests/nvidiagpu/deploy-gpu-test.go
* pkg/olm/almexamples.go
* pkg/nvidiagpu/clusterpolicy.go

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>README.md</summary>

81-81: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

94-94: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

95-95: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

96-96: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

97-97: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

98-98: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

99-99: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

100-100: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

101-101: Bare URL used
null

(MD034, no-bare-urls)

---

102-102: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

103-103: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

104-104: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

---

105-105: Unordered list style
Expected: asterisk; Actual: dash

(MD004, ul-style)

</details>

</details>
<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>README.md</summary>

[uncategorized] ~101-~101: Possible missing comma found.
Context: ...l also run the ib_write_bw command with arguments either in CLient or Server mode.  Defau...

(AI_HYDRA_LEO_MISSING_COMMA)

</details>

</details>

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T08:46:40Z

Review comments:
diff_hunk: @@ -0,0 +1,42 @@
+package olm
+
+import (
+	"encoding/json"
+	"fmt"
+
+	k8sjson "k8s.io/apimachinery/pkg/util/json"
+)
+
+// GetALMExampleItem retrieves the ALM example item at the specified index as a raw JSON,
+// which can be later unmarshalled into custom resource objects. This can be useful if
+// the original JSON needs to be modified, e.g. patched.
+func GetALMExampleItem(index int, almExample string) (json.RawMessage, error) {
+	examples, err := GetALMExampleItems(almExample)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(examples) < index+1 {
+		return nil, fmt.Errorf("no alm examples item exists at index %d", index)
+	}
+
+	return examples[index], nil
path: pkg/olm/almexamples.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Guard against negative indexes before array access**

`GetALMExampleItem` only checks that the slice length is at least `index+1`.  
If a caller accidentally passes a negative index, the comparison still passes (`len(examples) < -1` is false) and the subsequent `examples[index]` panics at runtime.

```diff
-	if len(examples) < index+1 {
+	if index < 0 || len(examples) < index+1 {
 		return nil, fmt.Errorf("no alm examples item exists at index %d", index)
 	}
```

A simple range-check avoids an unexpected crash and aligns the behaviour with the function’s error contract.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
	if index < 0 || len(examples) < index+1 {
		return nil, fmt.Errorf("no alm examples item exists at index %d", index)
	}

	return examples[index], nil
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In pkg/olm/almexamples.go around lines 19 to 23, the function GetALMExampleItem
does not check for negative index values before accessing the slice, which can
cause a runtime panic. Add a condition to verify that the index is non-negative
(index >= 0) along with the existing length check before accessing
examples[index]. If the index is negative or out of range, return an error
indicating the invalid index to prevent unexpected crashes.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T14:31:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,14 +32,44 @@ type Builder struct {
 
 // NewBuilderFromObjectString creates a Builder object from CSV alm-examples.
 func NewBuilderFromObjectString(apiClient *clients.Settings, almExample string) *Builder {
-	glog.V(100).Infof(
-		"Initializing new Builder structure from almExample string")
+	glog.V(100).Infof("Initializing new Builder structure from almExample string")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
+
+	err = k8sjson.Unmarshal(clusterPolicyExample, &clusterPolicy)
+	return newBuilder(apiClient, &clusterPolicy, err)
+}
+
+// NewBuilderFromObjectStringAndPatch creates a Builder object from CSV alm-examples and applies an RFC6902 JSON patch to it.
+func NewBuilderFromObjectStringAndPatch(apiClient *clients.Settings, almExample, patchJSON string) *Builder {
+	glog.V(100).Infof("Initializing new Builder structure from almExample string and a patch JSON")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
 
-	clusterPolicy, err := getClusterPolicyFromAlmExample(almExample)
+	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
path: pkg/nvidiagpu/clusterpolicy.go
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Handle empty or whitespace-only patch strings gracefully**

`jsonpatch.DecodePatch([]byte(""))` returns “unexpected end of JSON input”, which
turns a *no-patch* scenario into an error and blocks the builder creation.

Instead, detect an empty/blank patch early and fall back to the unpatched flow:

```diff
-	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
-	if err != nil {
-		return newBuilder(apiClient, &clusterPolicy, err)
-	}
+	if strings.TrimSpace(patchJSON) == "" {
+		glog.V(100).Info("No patch provided, using default ClusterPolicy")
+		if err := k8sjson.Unmarshal(clusterPolicyExample, &clusterPolicy); err != nil {
+			return newBuilder(apiClient, &clusterPolicy, err)
+		}
+		return newBuilder(apiClient, &clusterPolicy, nil)
+	}
+
+	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, fmt.Errorf("invalid JSON patch: %w", err))
+	}
```

(remember to add `strings` to the import list).

This makes the function tolerant to a missing env-var and surfaces invalid patch
syntax with a clearer message.  



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

```suggestion
	if strings.TrimSpace(patchJSON) == "" {
		glog.V(100).Info("No patch provided, using default ClusterPolicy")
		if err := k8sjson.Unmarshal(clusterPolicyExample, &clusterPolicy); err != nil {
			return newBuilder(apiClient, &clusterPolicy, err)
		}
		return newBuilder(apiClient, &clusterPolicy, nil)
	}

	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
	if err != nil {
		return newBuilder(apiClient, &clusterPolicy, fmt.Errorf("invalid JSON patch: %w", err))
	}
```

</details>

<!-- suggestion_end -->

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In pkg/nvidiagpu/clusterpolicy.go around lines 55 to 58, the code does not
handle empty or whitespace-only patch strings before calling
jsonpatch.DecodePatch, causing an error on no-patch scenarios. Fix this by
checking if patchJSON is empty or contains only whitespace using
strings.TrimSpace before decoding; if it is empty, skip decoding and proceed
with the unpatched flow. Add the "strings" package to imports to support this
check.
```

</details>

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-21T14:31:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,851 @@
+package jsonpatch
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+)
+
+const (
+	eRaw = iota
+	eDoc
+	eAry
+)
+
+var (
+	// SupportNegativeIndices decides whether to support non-standard practice of
+	// allowing negative indices to mean indices starting at the end of an array.
+	// Default to true.
+	SupportNegativeIndices bool = true
+	// AccumulatedCopySizeLimit limits the total size increase in bytes caused by
+	// "copy" operations in a patch.
+	AccumulatedCopySizeLimit int64 = 0
+)
+
+var (
+	ErrTestFailed   = errors.New("test failed")
+	ErrMissing      = errors.New("missing value")
+	ErrUnknownType  = errors.New("unknown object type")
+	ErrInvalid      = errors.New("invalid state detected")
+	ErrInvalidIndex = errors.New("invalid index referenced")
+)
+
+type lazyNode struct {
+	raw   *json.RawMessage
+	doc   partialDoc
+	ary   partialArray
+	which int
+}
+
+// Operation is a single JSON-Patch step, such as a single 'add' operation.
+type Operation map[string]*json.RawMessage
+
+// Patch is an ordered collection of Operations.
+type Patch []Operation
+
+type partialDoc map[string]*lazyNode
+type partialArray []*lazyNode
+
+type container interface {
+	get(key string) (*lazyNode, error)
+	set(key string, val *lazyNode) error
+	add(key string, val *lazyNode) error
+	remove(key string) error
+}
+
+func newLazyNode(raw *json.RawMessage) *lazyNode {
+	return &lazyNode{raw: raw, doc: nil, ary: nil, which: eRaw}
+}
+
+func (n *lazyNode) MarshalJSON() ([]byte, error) {
+	switch n.which {
+	case eRaw:
+		return json.Marshal(n.raw)
+	case eDoc:
+		return json.Marshal(n.doc)
+	case eAry:
+		return json.Marshal(n.ary)
+	default:
+		return nil, ErrUnknownType
+	}
+}
+
+func (n *lazyNode) UnmarshalJSON(data []byte) error {
+	dest := make(json.RawMessage, len(data))
+	copy(dest, data)
+	n.raw = &dest
+	n.which = eRaw
+	return nil
+}
+
+func deepCopy(src *lazyNode) (*lazyNode, int, error) {
+	if src == nil {
+		return nil, 0, nil
+	}
+	a, err := src.MarshalJSON()
+	if err != nil {
+		return nil, 0, err
+	}
+	sz := len(a)
+	ra := make(json.RawMessage, sz)
+	copy(ra, a)
+	return newLazyNode(&ra), sz, nil
+}
+
+func (n *lazyNode) intoDoc() (*partialDoc, error) {
+	if n.which == eDoc {
+		return &n.doc, nil
+	}
+
+	if n.raw == nil {
+		return nil, ErrInvalid
+	}
+
+	err := json.Unmarshal(*n.raw, &n.doc)
+
+	if err != nil {
+		return nil, err
+	}
+
+	n.which = eDoc
+	return &n.doc, nil
+}
+
+func (n *lazyNode) intoAry() (*partialArray, error) {
+	if n.which == eAry {
+		return &n.ary, nil
+	}
+
+	if n.raw == nil {
+		return nil, ErrInvalid
+	}
+
+	err := json.Unmarshal(*n.raw, &n.ary)
+
+	if err != nil {
+		return nil, err
+	}
+
+	n.which = eAry
+	return &n.ary, nil
+}
+
+func (n *lazyNode) compact() []byte {
+	buf := &bytes.Buffer{}
+
+	if n.raw == nil {
+		return nil
+	}
+
+	err := json.Compact(buf, *n.raw)
+
+	if err != nil {
+		return *n.raw
+	}
+
+	return buf.Bytes()
+}
+
+func (n *lazyNode) tryDoc() bool {
+	if n.raw == nil {
+		return false
+	}
+
+	err := json.Unmarshal(*n.raw, &n.doc)
+
+	if err != nil {
+		return false
+	}
+
+	n.which = eDoc
+	return true
+}
+
+func (n *lazyNode) tryAry() bool {
+	if n.raw == nil {
+		return false
+	}
+
+	err := json.Unmarshal(*n.raw, &n.ary)
+
+	if err != nil {
+		return false
+	}
+
+	n.which = eAry
+	return true
+}
+
+func (n *lazyNode) equal(o *lazyNode) bool {
+	if n.which == eRaw {
+		if !n.tryDoc() && !n.tryAry() {
+			if o.which != eRaw {
+				return false
+			}
+
+			return bytes.Equal(n.compact(), o.compact())
+		}
+	}
+
+	if n.which == eDoc {
+		if o.which == eRaw {
+			if !o.tryDoc() {
+				return false
+			}
+		}
+
+		if o.which != eDoc {
+			return false
+		}
+
+		if len(n.doc) != len(o.doc) {
+			return false
+		}
+
+		for k, v := range n.doc {
+			ov, ok := o.doc[k]
+
+			if !ok {
+				return false
+			}
+
+			if (v == nil) != (ov == nil) {
+				return false
+			}
+
+			if v == nil && ov == nil {
+				continue
+			}
+
+			if !v.equal(ov) {
+				return false
+			}
+		}
+
+		return true
+	}
+
+	if o.which != eAry && !o.tryAry() {
+		return false
+	}
+
+	if len(n.ary) != len(o.ary) {
+		return false
+	}
+
+	for idx, val := range n.ary {
+		if !val.equal(o.ary[idx]) {
+			return false
+		}
+	}
+
+	return true
+}
+
+// Kind reads the "op" field of the Operation.
+func (o Operation) Kind() string {
+	if obj, ok := o["op"]; ok && obj != nil {
+		var op string
+
+		err := json.Unmarshal(*obj, &op)
+
+		if err != nil {
+			return "unknown"
+		}
+
+		return op
+	}
+
+	return "unknown"
+}
+
+// Path reads the "path" field of the Operation.
+func (o Operation) Path() (string, error) {
+	if obj, ok := o["path"]; ok && obj != nil {
+		var op string
+
+		err := json.Unmarshal(*obj, &op)
+
+		if err != nil {
+			return "unknown", err
+		}
+
+		return op, nil
+	}
+
+	return "unknown", errors.Wrapf(ErrMissing, "operation missing path field")
+}
+
+// From reads the "from" field of the Operation.
+func (o Operation) From() (string, error) {
+	if obj, ok := o["from"]; ok && obj != nil {
+		var op string
+
+		err := json.Unmarshal(*obj, &op)
+
+		if err != nil {
+			return "unknown", err
+		}
+
+		return op, nil
+	}
+
+	return "unknown", errors.Wrapf(ErrMissing, "operation, missing from field")
+}
+
+func (o Operation) value() *lazyNode {
+	if obj, ok := o["value"]; ok {
+		return newLazyNode(obj)
+	}
+
+	return nil
+}
+
+// ValueInterface decodes the operation value into an interface.
+func (o Operation) ValueInterface() (interface{}, error) {
+	if obj, ok := o["value"]; ok && obj != nil {
+		var v interface{}
+
+		err := json.Unmarshal(*obj, &v)
+
+		if err != nil {
+			return nil, err
+		}
+
+		return v, nil
+	}
+
+	return nil, errors.Wrapf(ErrMissing, "operation, missing value field")
+}
+
+func isArray(buf []byte) bool {
+Loop:
+	for _, c := range buf {
+		switch c {
+		case ' ':
+		case '\n':
+		case '\t':
+			continue
+		case '[':
+			return true
+		default:
+			break Loop
+		}
+	}
+
+	return false
+}
+
+func findObject(pd *container, path string) (container, string) {
+	doc := *pd
+
+	split := strings.Split(path, "/")
+
+	if len(split) < 2 {
+		return nil, ""
+	}
+
+	parts := split[1 : len(split)-1]
+
+	key := split[len(split)-1]
+
+	var err error
+
+	for _, part := range parts {
+
+		next, ok := doc.get(decodePatchKey(part))
+
+		if next == nil || ok != nil || next.raw == nil {
+			return nil, ""
+		}
+
+		if isArray(*next.raw) {
+			doc, err = next.intoAry()
+
+			if err != nil {
+				return nil, ""
+			}
+		} else {
+			doc, err = next.intoDoc()
+
+			if err != nil {
+				return nil, ""
+			}
+		}
+	}
+
+	return doc, decodePatchKey(key)
+}
+
+func (d *partialDoc) set(key string, val *lazyNode) error {
+	(*d)[key] = val
+	return nil
+}
+
+func (d *partialDoc) add(key string, val *lazyNode) error {
+	(*d)[key] = val
+	return nil
+}
+
+func (d *partialDoc) get(key string) (*lazyNode, error) {
+	return (*d)[key], nil
+}
+
+func (d *partialDoc) remove(key string) error {
+	_, ok := (*d)[key]
+	if !ok {
+		return errors.Wrapf(ErrMissing, "Unable to remove nonexistent key: %s", key)
+	}
+
+	delete(*d, key)
+	return nil
+}
+
+// set should only be used to implement the "replace" operation, so "key" must
+// be an already existing index in "d".
+func (d *partialArray) set(key string, val *lazyNode) error {
+	idx, err := strconv.Atoi(key)
+	if err != nil {
+		return err
+	}
+
+	if idx < 0 {
+		if !SupportNegativeIndices {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		if idx < -len(*d) {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		idx += len(*d)
+	}
+
+	(*d)[idx] = val
+	return nil
+}
+
+func (d *partialArray) add(key string, val *lazyNode) error {
+	if key == "-" {
+		*d = append(*d, val)
+		return nil
+	}
+
+	idx, err := strconv.Atoi(key)
+	if err != nil {
+		return errors.Wrapf(err, "value was not a proper array index: '%s'", key)
+	}
+
+	sz := len(*d) + 1
+
+	ary := make([]*lazyNode, sz)
+
+	cur := *d
+
+	if idx >= len(ary) {
+		return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+	}
+
+	if idx < 0 {
+		if !SupportNegativeIndices {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		if idx < -len(ary) {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		idx += len(ary)
+	}
+
+	copy(ary[0:idx], cur[0:idx])
+	ary[idx] = val
+	copy(ary[idx+1:], cur[idx:])
+
+	*d = ary
+	return nil
+}
+
+func (d *partialArray) get(key string) (*lazyNode, error) {
+	idx, err := strconv.Atoi(key)
+
+	if err != nil {
+		return nil, err
+	}
+
+	if idx < 0 {
+		if !SupportNegativeIndices {
+			return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		if idx < -len(*d) {
+			return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		idx += len(*d)
+	}
+
+	if idx >= len(*d) {
+		return nil, errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+	}
+
+	return (*d)[idx], nil
+}
+
+func (d *partialArray) remove(key string) error {
+	idx, err := strconv.Atoi(key)
+	if err != nil {
+		return err
+	}
+
+	cur := *d
+
+	if idx >= len(cur) {
+		return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+	}
+
+	if idx < 0 {
+		if !SupportNegativeIndices {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		if idx < -len(cur) {
+			return errors.Wrapf(ErrInvalidIndex, "Unable to access invalid index: %d", idx)
+		}
+		idx += len(cur)
+	}
+
+	ary := make([]*lazyNode, len(cur)-1)
+
+	copy(ary[0:idx], cur[0:idx])
+	copy(ary[idx:], cur[idx+1:])
+
+	*d = ary
+	return nil
+
+}
+
+func (p Patch) add(doc *container, op Operation) error {
+	path, err := op.Path()
+	if err != nil {
+		return errors.Wrapf(ErrMissing, "add operation failed to decode path")
+	}
+
+	con, key := findObject(doc, path)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "add operation does not apply: doc is missing path: \"%s\"", path)
+	}
+
+	err = con.add(key, op.value())
+	if err != nil {
+		return errors.Wrapf(err, "error in add for path: '%s'", path)
+	}
+
+	return nil
+}
+
+func (p Patch) remove(doc *container, op Operation) error {
+	path, err := op.Path()
+	if err != nil {
+		return errors.Wrapf(ErrMissing, "remove operation failed to decode path")
+	}
+
+	con, key := findObject(doc, path)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "remove operation does not apply: doc is missing path: \"%s\"", path)
+	}
+
+	err = con.remove(key)
+	if err != nil {
+		return errors.Wrapf(err, "error in remove for path: '%s'", path)
+	}
+
+	return nil
+}
+
+func (p Patch) replace(doc *container, op Operation) error {
+	path, err := op.Path()
+	if err != nil {
+		return errors.Wrapf(err, "replace operation failed to decode path")
+	}
+
+	if path == "" {
+		val := op.value()
+
+		if val.which == eRaw {
+			if !val.tryDoc() {
+				if !val.tryAry() {
+					return errors.Wrapf(err, "replace operation value must be object or array")
+				}
+			}
+		}
+
+		switch val.which {
+		case eAry:
+			*doc = &val.ary
+		case eDoc:
+			*doc = &val.doc
+		case eRaw:
+			return errors.Wrapf(err, "replace operation hit impossible case")
+		}
+
+		return nil
+	}
+
+	con, key := findObject(doc, path)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing path: %s", path)
+	}
+
+	_, ok := con.get(key)
+	if ok != nil {
+		return errors.Wrapf(ErrMissing, "replace operation does not apply: doc is missing key: %s", path)
+	}
+
+	err = con.set(key, op.value())
+	if err != nil {
+		return errors.Wrapf(err, "error in remove for path: '%s'", path)
+	}
+
+	return nil
+}
+
+func (p Patch) move(doc *container, op Operation) error {
+	from, err := op.From()
+	if err != nil {
+		return errors.Wrapf(err, "move operation failed to decode from")
+	}
+
+	con, key := findObject(doc, from)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing from path: %s", from)
+	}
+
+	val, err := con.get(key)
+	if err != nil {
+		return errors.Wrapf(err, "error in move for path: '%s'", key)
+	}
+
+	err = con.remove(key)
+	if err != nil {
+		return errors.Wrapf(err, "error in move for path: '%s'", key)
+	}
+
+	path, err := op.Path()
+	if err != nil {
+		return errors.Wrapf(err, "move operation failed to decode path")
+	}
+
+	con, key = findObject(doc, path)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "move operation does not apply: doc is missing destination path: %s", path)
+	}
+
+	err = con.add(key, val)
+	if err != nil {
+		return errors.Wrapf(err, "error in move for path: '%s'", path)
+	}
+
+	return nil
+}
+
+func (p Patch) test(doc *container, op Operation) error {
+	path, err := op.Path()
+	if err != nil {
+		return errors.Wrapf(err, "test operation failed to decode path")
+	}
+
+	if path == "" {
+		var self lazyNode
+
+		switch sv := (*doc).(type) {
+		case *partialDoc:
+			self.doc = *sv
+			self.which = eDoc
+		case *partialArray:
+			self.ary = *sv
+			self.which = eAry
+		}
+
+		if self.equal(op.value()) {
+			return nil
+		}
+
+		return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+	}
+
+	con, key := findObject(doc, path)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "test operation does not apply: is missing path: %s", path)
+	}
+
+	val, err := con.get(key)
+	if err != nil {
+		return errors.Wrapf(err, "error in test for path: '%s'", path)
+	}
+
+	if val == nil {
+		if op.value() == nil || op.value().raw == nil {
+			return nil
+		}
+		return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+	} else if op.value() == nil {
+		return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+	}
+
+	if val.equal(op.value()) {
+		return nil
+	}
+
+	return errors.Wrapf(ErrTestFailed, "testing value %s failed", path)
+}
+
+func (p Patch) copy(doc *container, op Operation, accumulatedCopySize *int64) error {
+	from, err := op.From()
+	if err != nil {
+		return errors.Wrapf(err, "copy operation failed to decode from")
+	}
+
+	con, key := findObject(doc, from)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing from path: %s", from)
+	}
+
+	val, err := con.get(key)
+	if err != nil {
+		return errors.Wrapf(err, "error in copy for from: '%s'", from)
+	}
+
+	path, err := op.Path()
+	if err != nil {
+		return errors.Wrapf(ErrMissing, "copy operation failed to decode path")
+	}
+
+	con, key = findObject(doc, path)
+
+	if con == nil {
+		return errors.Wrapf(ErrMissing, "copy operation does not apply: doc is missing destination path: %s", path)
+	}
+
+	valCopy, sz, err := deepCopy(val)
+	if err != nil {
+		return errors.Wrapf(err, "error while performing deep copy")
+	}
+
+	(*accumulatedCopySize) += int64(sz)
+	if AccumulatedCopySizeLimit > 0 && *accumulatedCopySize > AccumulatedCopySizeLimit {
+		return NewAccumulatedCopySizeError(AccumulatedCopySizeLimit, *accumulatedCopySize)
+	}
+
+	err = con.add(key, valCopy)
+	if err != nil {
+		return errors.Wrapf(err, "error while adding value during copy")
+	}
+
+	return nil
+}
+
+// Equal indicates if 2 JSON documents have the same structural equality.
+func Equal(a, b []byte) bool {
+	ra := make(json.RawMessage, len(a))
+	copy(ra, a)
+	la := newLazyNode(&ra)
+
+	rb := make(json.RawMessage, len(b))
+	copy(rb, b)
+	lb := newLazyNode(&rb)
+
+	return la.equal(lb)
+}
+
+// DecodePatch decodes the passed JSON document as an RFC 6902 patch.
+func DecodePatch(buf []byte) (Patch, error) {
+	var p Patch
+
+	err := json.Unmarshal(buf, &p)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return p, nil
+}
path: vendor/github.com/evanphx/json-patch/patch.go
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**`DecodePatch` no longer validates JSON Patch structure**  

The `/v5` implementation performs both `json.Valid` and
`validatePatch(...)` checks, rejecting malformed RFC-6902 documents.
This copy only does a blind `json.Unmarshal`, letting many invalid
patches slip through and potentially crash later in `Apply`.

```diff
-func DecodePatch(buf []byte) (Patch, error) {
-    var p Patch
-    err := json.Unmarshal(buf, &p)
-    if err != nil {
-        return nil, err
-    }
-    return p, nil
-}
+func DecodePatch(buf []byte) (Patch, error) {
+    if !json.Valid(buf) {
+        return nil, ErrInvalid
+    }
+    var p Patch
+    if err := json.Unmarshal(buf, &p); err != nil {
+        return nil, err
+    }
+    if err := validatePatch(p); err != nil { // port over from /v5
+        return nil, err
+    }
+    return p, nil
+}
```

Without this guard the operator can accept malformed patches and fail
at runtime.  Please re-add the validation logic.



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In vendor/github.com/evanphx/json-patch/patch.go around lines 766 to 776, the
DecodePatch function currently only unmarshals JSON without validating the patch
structure. To fix this, add a json.Valid check on the input buffer and call
validatePatch on the unmarshaled Patch before returning it. If either check
fails, return an error to prevent malformed patches from being accepted and
causing runtime failures.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T14:31:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,851 @@
+package jsonpatch
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"strings"
+
+	"github.com/pkg/errors"
+)
+
path: vendor/github.com/evanphx/json-patch/patch.go
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Duplicate JSON-Patch libraries introduce build-time ambiguity**  

The repository already vendors `github.com/evanphx/json-patch/v5/*`.  
Adding the *non-versioned* package side-by-side means we now have **two different
module paths that both declare `package jsonpatch`**.  Any `import` in the
codebase that simply uses:

```go
import "github.com/evanphx/json-patch/jsonpatch"
```

will silently select the non-versioned copy, while
`import "github.com/evanphx/json-patch/v5"` will resolve to the other one.
Besides code duplication and binary bloat, this is very likely to cause:

* compilation errors due to ambiguous imports or missing symbols
* runtime drift – the two versions have subtly different behaviour
  (see `DecodePatch` differences below)
* vendor/ vs. `go.sum` confusion for downstream consumers

Please pick **one** module path (preferably the maintained `/v5` one),
remove the other, and adjust imports accordingly.

---

🏁 Script executed:

```shell
#!/usr/bin/env bash
# List all imports of the two json-patch module paths
rg --no-heading -n $'github.com/evanphx/json-patch(/v5)?' | sort -u
```

Length of output: 4068

---



</details>

**Consolidate JSON-Patch to the v5 module and remove the legacy copy**

We currently vendor and import both  
  • github.com/evanphx/json-patch (v5.7.0+incompatible)  
  • github.com/evanphx/json-patch/v5 (v5.9.11)  

This leads to two `package jsonpatch` definitions, ambiguous resolution, binary bloat, and drift between patch implementations. Let’s standardize on v5:

• pkg/nvidiagpu/clusterpolicy.go  
  ```diff
  - import jsonpatch "github.com/evanphx/json-patch"
  + import jsonpatch "github.com/evanphx/json-patch/v5"
  ```

• go.mod  
  – Remove the `github.com/evanphx/json-patch v5.7.0+incompatible` line.  
  – Ensure only `github.com/evanphx/json-patch/v5 v5.9.11` remains (run `go get github.com/evanphx/json-patch/v5` then `go mod tidy`).

• Vendor directory  
  – Delete `vendor/github.com/evanphx/json-patch/…` (the non-v5 tree).  
  – Regenerate with `go mod vendor` so only v5 files are vendored.

After these changes, all imports of `jsonpatch` will resolve to the maintained v5 path and eliminate duplication.  


> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
In vendor/github.com/evanphx/json-patch/patch.go lines 1 to 12, the code vendors
and imports the non-versioned json-patch package alongside the v5 version,
causing duplicate package definitions and ambiguity. To fix this, remove the
entire non-versioned json-patch package from the vendor directory, update go.mod
to remove the old module line and keep only the v5 version, adjust all imports
in the codebase to use "github.com/evanphx/json-patch/v5", and regenerate the
vendor directory with `go mod vendor` to ensure only the v5 package is included.
```

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-21T14:31:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -30,14 +32,44 @@ type Builder struct {
 
 // NewBuilderFromObjectString creates a Builder object from CSV alm-examples.
 func NewBuilderFromObjectString(apiClient *clients.Settings, almExample string) *Builder {
-	glog.V(100).Infof(
-		"Initializing new Builder structure from almExample string")
+	glog.V(100).Infof("Initializing new Builder structure from almExample string")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
+
+	err = k8sjson.Unmarshal(clusterPolicyExample, &clusterPolicy)
+	return newBuilder(apiClient, &clusterPolicy, err)
+}
+
+// NewBuilderFromObjectStringAndPatch creates a Builder object from CSV alm-examples and applies an RFC6902 JSON patch to it.
+func NewBuilderFromObjectStringAndPatch(apiClient *clients.Settings, almExample, patchJSON string) *Builder {
+	glog.V(100).Infof("Initializing new Builder structure from almExample string and a patch JSON")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
 
-	clusterPolicy, err := getClusterPolicyFromAlmExample(almExample)
+	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
path: pkg/nvidiagpu/clusterpolicy.go
user: login: empovit
body: Treat an empty patch as an error, as we do not know if passing an empty value in the env variable was intentional or by mistake.
created_at: 2025-05-21T14:55:31Z
author_association: MEMBER
diff_hunk: @@ -30,14 +32,44 @@ type Builder struct {
 
 // NewBuilderFromObjectString creates a Builder object from CSV alm-examples.
 func NewBuilderFromObjectString(apiClient *clients.Settings, almExample string) *Builder {
-	glog.V(100).Infof(
-		"Initializing new Builder structure from almExample string")
+	glog.V(100).Infof("Initializing new Builder structure from almExample string")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
+
+	err = k8sjson.Unmarshal(clusterPolicyExample, &clusterPolicy)
+	return newBuilder(apiClient, &clusterPolicy, err)
+}
+
+// NewBuilderFromObjectStringAndPatch creates a Builder object from CSV alm-examples and applies an RFC6902 JSON patch to it.
+func NewBuilderFromObjectStringAndPatch(apiClient *clients.Settings, almExample, patchJSON string) *Builder {
+	glog.V(100).Infof("Initializing new Builder structure from almExample string and a patch JSON")
+	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
 
-	clusterPolicy, err := getClusterPolicyFromAlmExample(almExample)
+	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
+	if err != nil {
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}
path: pkg/nvidiagpu/clusterpolicy.go
user: login: coderabbitai[bot]
body: @empovit I understand you want to treat an empty patch as an error to ensure users are aware of potential configuration mistakes. I suggest adding an explicit check before decoding:

```diff
func NewBuilderFromObjectStringAndPatch(apiClient *clients.Settings, almExample, patchJSON string) *Builder {
	glog.V(100).Infof("Initializing new Builder structure from almExample string and a patch JSON")
	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	if strings.TrimSpace(patchJSON) == "" {
+		err := fmt.Errorf("patch JSON cannot be an empty string")
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}

	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
	if err != nil {
		return newBuilder(apiClient, &clusterPolicy, err)
	}

	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
```

This provides a clear error message rather than relying on the generic parsing error from `DecodePatch`.


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@empovit I understand you want to treat an empty patch as an error to ensure users are aware of potential configuration mistakes. I suggest adding an explicit check before decoding:

```diff
func NewBuilderFromObjectStringAndPatch(apiClient *clients.Settings, almExample, patchJSON string) *Builder {
	glog.V(100).Infof("Initializing new Builder structure from almExample string and a patch JSON")
	var clusterPolicy nvidiagpuv1.ClusterPolicy
+	if strings.TrimSpace(patchJSON) == "" {
+		err := fmt.Errorf("patch JSON cannot be an empty string")
+		return newBuilder(apiClient, &clusterPolicy, err)
+	}

	clusterPolicyExample, err := olm.GetALMExampleItem(0, almExample)
	if err != nil {
		return newBuilder(apiClient, &clusterPolicy, err)
	}

	patch, err := jsonpatch.DecodePatch([]byte(patchJSON))
```

This provides a clear error message rather than relying on the generic parsing error from `DecodePatch`.</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-21T14:56:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -627,7 +627,14 @@ var _ = Describe("GPU", Ordered, Label(tsparams.LabelSuite), func() {
 
 			By("Deploy ClusterPolicy")
 			glog.V(gpuparams.GpuLogLevel).Infof("Creating ClusterPolicy from CSV almExamples")
-			clusterPolicyBuilder := nvidiagpu.NewBuilderFromObjectString(inittools.APIClient, almExamples)
+
+			var clusterPolicyBuilder *nvidiagpu.Builder
+			if nvidiaGPUConfig.ClusterPolicyPatch == "" {
+				clusterPolicyBuilder = nvidiagpu.NewBuilderFromObjectString(inittools.APIClient, almExamples)
+			} else {
+				clusterPolicyBuilder = nvidiagpu.NewBuilderFromObjectStringAndPatch(inittools.APIClient, almExamples, nvidiaGPUConfig.ClusterPolicyPatch)
path: tests/nvidiagpu/deploy-gpu-test.go
user: login: wabouhamad
body: Would be good to add a glog message to mention we are using a patched clusterpolicy and not the default one from alm-example.   This helps in troubleshooting.   Also would be more consistent if we move the check for this env var in the BeforeAll() section where we process all the input env vars and log the input values from NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH env var in the beginning of the log, for better troubleshooting.
created_at: 2025-05-21T20:39:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,42 @@
+package olm
+
+import (
+	"encoding/json"
+	"fmt"
+
+	k8sjson "k8s.io/apimachinery/pkg/util/json"
+)
+
+// GetALMExampleItem retrieves the ALM example item at the specified index as a raw JSON,
+// which can be later unmarshalled into custom resource objects. This can be useful if
+// the original JSON needs to be modified, e.g. patched.
+func GetALMExampleItem(index int, almExample string) (json.RawMessage, error) {
+	examples, err := GetALMExampleItems(almExample)
+	if err != nil {
+		return nil, err
path: pkg/olm/almexamples.go
user: login: TomerNewman
body: ```suggestion
		return nil, fmt.ErrorF("failed to get example items: %v", err)
```
created_at: 2025-05-22T06:20:03Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,42 @@
+package olm
+
+import (
+	"encoding/json"
+	"fmt"
+
+	k8sjson "k8s.io/apimachinery/pkg/util/json"
+)
+
+// GetALMExampleItem retrieves the ALM example item at the specified index as a raw JSON,
+// which can be later unmarshalled into custom resource objects. This can be useful if
+// the original JSON needs to be modified, e.g. patched.
+func GetALMExampleItem(index int, almExample string) (json.RawMessage, error) {
+	examples, err := GetALMExampleItems(almExample)
+	if err != nil {
+		return nil, err
path: pkg/olm/almexamples.go
user: login: TomerNewman
body: IMO this way it will be easier to debug it later
created_at: 2025-05-22T06:20:22Z
author_association: MEMBER

PR comments:
user: login: openshift-ci[bot]
created_at: 2025-05-21T14:24:01Z
body: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *<a href="https://github.com/rh-ecosystem-edge/nvidia-ci/pull/182#" title="Author self-approved">empovit</a>*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=rh-ecosystem-edge%2Fnvidia-ci).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com/rh-ecosystem-edge/nvidia-ci/blob/main/OWNERS)~~ [empovit]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-21T14:24:02Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces support for applying RFC 6902 JSON patches to the default NVIDIA GPU ClusterPolicy using a new environment variable. It adds patch handling logic, new helper functions for ALM example extraction, updates documentation, and modifies dependencies to include a newer version of the `json-patch` library.

## Changes

| File(s)                                                                                  | Change Summary |
|------------------------------------------------------------------------------------------|---------------|
| README.md                                                                                | Added documentation for the new `NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH` environment variable; fixed formatting in NVIDIA Network Operator section. |
| go.mod, vendor/modules.txt                                                               | Updated `github.com/evanphx/json-patch/v5` dependency from v5.9.0 to v5.9.11. |
| internal/nvidiagpuconfig/config.go                                                       | Added `ClusterPolicyPatch` field to `NvidiaGPUConfig` struct with environment variable tag. |
| pkg/nvidiagpu/clusterpolicy.go                                                           | Replaced custom ALM example extraction with new helper from `olm` package; added support for applying JSON patches; consolidated builder logic; removed obsolete function. |
| pkg/olm/almexamples.go                                                                   | New file: Provides functions to extract and access ALM example items as raw JSON. |
| tests/nvidiagpu/deploy-gpu-test.go                                                      | Updated test logic to optionally apply a patch to ClusterPolicy using the new environment variable. |
| vendor/github.com/evanphx/json-patch/v5/merge.go                                         | Renamed internal error variables to exported identifiers for better error handling. |
| vendor/github.com/evanphx/json-patch/v5/patch.go                                         | Replaced `github.com/pkg/errors` with standard `fmt.Errorf` for error wrapping; updated error handling accordingly. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant Env
    participant Test
    participant Builder
    participant OLM
    participant JSONPatch

    User->>Env: Set NVIDIAGPU_GPU_CLUSTER_POLICY_PATCH (optional)
    Test->>Env: Read ClusterPolicyPatch variable
    alt Patch is set
        Test->>Builder: NewBuilderFromObjectStringAndPatch(almExample, patch)
        Builder->>OLM: GetALMExampleItem(almExample)
        OLM-->>Builder: ALM example JSON
        Builder->>JSONPatch: DecodePatch(patch)
        JSONPatch-->>Builder: RFC6902 Patch
        Builder->>Builder: Apply patch to ALM example JSON
        Builder->>Builder: Unmarshal to ClusterPolicy
    else No patch
        Test->>Builder: NewBuilderFromObjectString(almExample)
        Builder->>OLM: GetALMExampleItem(almExample)
        OLM-->>Builder: ALM example JSON
        Builder->>Builder: Unmarshal to ClusterPolicy
    end
    Builder-->>Test: ClusterPolicy Builder
```

## Suggested labels

`lgtm`

## Suggested reviewers

- wabouhamad  
- fabiendupont

## Poem

> In the warren where code is patched and spun,  
> A new env var hops in—oh, what fun!  
> ClusterPolicy now bends to your JSON will,  
> With helpers and tests that fit the bill.  
> The garden grows with patches anew—  
> A bunny’s delight in what devs can do! 🐇

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!NOTE]
> <details>
> <summary>⚡️ AI Code Reviews for VS Code, Cursor, Windsurf</summary>
> 
> CodeRabbit now has a plugin for VS Code, Cursor and Windsurf. This brings AI code reviews directly in the code editor. Each commit is reviewed immediately, finding bugs before the PR is raised. Seamless context handoff to your AI code agent ensures that you can easily incorporate review feedback.
> Learn more [here](http://coderabbit.ai/ide).
> 
> </details>

---

> [!NOTE]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 30th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=rh-ecosystem-edge/nvidia-ci&utm_content=182):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: empovit
created_at: 2025-05-21T14:24:09Z
body: cc @TomerNewman 
author_association: MEMBER
user: login: empovit
created_at: 2025-05-21T16:35:43Z
body: @wabouhamad @TomerNewman this PR has been tested and is ready for review. The CodeRabbit's comments have been addressed.
author_association: MEMBER
user: login: TomerNewman
created_at: 2025-05-22T09:10:45Z
body: /lgtm
author_association: MEMBER