Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 47 additions & 8 deletions pkg/monitortestlibrary/platformidentification/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import (
exutil "github.com/openshift/origin/test/extended/util"
kapierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
Expand Down Expand Up @@ -149,10 +152,13 @@ func BuildClusterData(ctx context.Context, clientConfig *rest.Config) (ClusterDa
clusterData.CloudZone = kNodes.Items[0].Labels[`topology.kubernetes.io/zone`]
}

if mcClient, err := machineconfigclient.NewForConfig(clientConfig); err != nil {
dynClient, err := dynamic.NewForConfig(clientConfig)
if err != nil {
errors = append(errors, err)
} else if mcClient, err := machineconfigclient.NewForConfig(clientConfig); err != nil {
errors = append(errors, err)
} else {
osImageStreams, err := getOSImageStreams(mcClient)
osImageStreams, err := getOSImageStreams(mcClient, dynClient)
clusterData.OS = OS{
OSImageStreams: osImageStreams,
}
Expand Down Expand Up @@ -193,17 +199,50 @@ func getClusterVersions(versions *configv1.ClusterVersionList) []string {
return cvs
}

func getOSImageStreams(mc machineconfigclient.Interface) (OSImageStreams, error) {
// GetOSImageStreamDefaultStream fetches the OSImageStream "cluster" singleton using the
// dynamic client, trying v1 first and falling back to v1alpha1. It returns the
// status.defaultStream value. Returns ("", nil) if the resource is not found.
func GetOSImageStreamDefaultStream(dc dynamic.Interface) (string, error) {
var obj *unstructured.Unstructured
var err error
for _, version := range []string{"v1", "v1alpha1"} {
osisGVR := schema.GroupVersionResource{
Group: "machineconfiguration.openshift.io",
Version: version,
Resource: "osimagestreams",
}
obj, err = dc.Resource(osisGVR).Get(context.TODO(), "cluster", metav1.GetOptions{})
if err == nil || !kapierrs.IsNotFound(err) {
Comment on lines +205 to +215
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate caller context and guard nil dynamic client in the singleton GET path.

Line 214 uses context.TODO(), so cancellation/timeouts from callers are ignored on this external API call. Also, this exported helper should reject nil dynamic.Interface instead of panicking on dc.Resource(...).

Proposed fix
-func GetOSImageStreamDefaultStream(dc dynamic.Interface) (string, error) {
+func GetOSImageStreamDefaultStream(ctx context.Context, dc dynamic.Interface) (string, error) {
+	if dc == nil {
+		return "", fmt.Errorf("dynamic client is nil")
+	}
 	var obj *unstructured.Unstructured
 	var err error
 	for _, version := range []string{"v1", "v1alpha1"} {
 		osisGVR := schema.GroupVersionResource{
 			Group:    "machineconfiguration.openshift.io",
 			Version:  version,
 			Resource: "osimagestreams",
 		}
-		obj, err = dc.Resource(osisGVR).Get(context.TODO(), "cluster", metav1.GetOptions{})
+		obj, err = dc.Resource(osisGVR).Get(ctx, "cluster", metav1.GetOptions{})
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/monitortestlibrary/platformidentification/types.go` around lines 205 -
215, GetOSImageStreamDefaultStream currently ignores caller context and can
panic when passed a nil dynamic client; change its signature to accept a
context.Context (e.g., ctx) and use that instead of context.TODO() for the
dc.Resource(...).Get call, and add a nil check for the dynamic.Interface
parameter (dc) at function start returning a clear error if nil to avoid a
panic; keep the rest of the loop and error handling unchanged and update all
call sites accordingly to pass through the caller's context.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not agree with this review comment. Already existing similar code, like the users of that function extensively use TODO() as the context. Adding the right context add little or no value.
About the nil check: Is not a pattern I've seen in that part of the codebase, so, given the code is testing code (the repo itself is a testing repo) and that there are no consistency issues between this code and the code near to it I think the best is to preserve it as it it. A nil deference error is usually very clean and easy to diagnose.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Resolve the review comment via GitHub CLI
gh pr view 31220 --repo openshift/origin --json reviews,comments 2>/dev/null | head -5

# List review comments by coderabbitai on this PR to find the comment ID
gh api repos/openshift/origin/pulls/31220/comments --jq '.[] | select(.user.login == "coderabbitai[bot]") | {id: .id, body: .body[0:80]}'

Length of output: 33490


🏁 Script executed:

#!/bin/bash
# Resolve the coderabbitai review comment
gh api repos/openshift/origin/pulls/31220/comments/3309900100 \
  --method PATCH \
  --field body="$(gh api repos/openshift/origin/pulls/31220/comments/3309900100 --jq '.body')" 2>/dev/null || true

# Actually, resolve via the GraphQL minimizeComment mutation - first get node id
gh api graphql -f query='
{
  repository(owner: "openshift", name: "origin") {
    pullRequest(number: 31220) {
      reviewThreads(first: 50) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              id
              databaseId
              author { login }
            }
          }
        }
      }
    }
  }
}' | jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.comments.nodes[0].databaseId == 3309900100) | {threadId: .id, commentId: .comments.nodes[0].id, isResolved: .isResolved}'

Length of output: 1179


🏁 Script executed:

#!/bin/bash
# Resolve the review thread using GraphQL
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "PRRT_kwDOAVZyrM6FDsvR"}) {
    thread {
      id
      isResolved
    }
  }
}'

Length of output: 719



@pablintino, understood on both points — the context.TODO() convention is consistent with the surrounding codebase, and a nil dereference is a perfectly clear, acceptable failure mode for test infrastructure code. I'll withdraw this comment.


✏️ Learnings added
Learnt from: pablintino
Repo: openshift/origin PR: 31220
File: pkg/monitortestlibrary/platformidentification/types.go:205-215
Timestamp: 2026-05-27T14:14:35.987Z
Learning: In the openshift/origin testing repository (e.g., `pkg/monitortestlibrary/platformidentification/types.go`), using `context.TODO()` for dynamic/API client calls is the established codebase convention — do not flag it as a missing context propagation issue. Additionally, adding explicit nil-guard checks on client interfaces (such as `dynamic.Interface`) is not a pattern used in this codebase; nil dereference panics are considered acceptable and easy to diagnose in this testing context.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


🧠 Learnings used
Learnt from: Chandan9112
Repo: openshift/origin PR: 31182
File: test/extended/node/node_e2e/node.go:0-0
Timestamp: 2026-05-19T12:28:55.867Z
Learning: When reviewing Go code that uses OpenShift configv1 (`github.com/openshift/api/config/v1`), note that `ImageSpec.RegistrySources` is declared as a non-pointer value (`RegistrySources RegistrySources`). It is always safely zero-initialized by Go, so it can never be `nil`. Therefore, do not add nil-guards for `imageSpec.RegistrySources` before accessing its fields (e.g., `AllowedRegistries`, `ContainerRuntimeSearchRegistries`).

Learnt from: Chandan9112
Repo: openshift/origin PR: 31182
File: test/extended/node/node_e2e/node.go:0-0
Timestamp: 2026-05-19T12:28:55.867Z
Learning: When using the OpenShift `configv1` API (`github.com/openshift/api/config/v1`), treat `ImageSpec.RegistrySources` as a non-nil Go struct value (`RegistrySources`, not `*RegistrySources`). Because it can never be nil (it’s always zero-initialized), don’t add nil-guards before accessing its fields (e.g., `AllowedRegistries`, `ContainerRuntimeSearchRegistries`). You may still need to handle zero-value contents, but a nil check on `RegistrySources` itself is unnecessary.

break
}
}
if err != nil {
if kapierrs.IsNotFound(err) {
return "", nil
}
return "", fmt.Errorf("error getting OSImageStream singleton: %v", err)
}
if obj == nil {
return "", fmt.Errorf("OSImageStream singleton returned nil object")
}
defaultStream, found, nestedErr := unstructured.NestedString(obj.Object, "status", "defaultStream")
if nestedErr != nil || !found {
return "", fmt.Errorf("error reading OSImageStream status.defaultStream: %v", nestedErr)
}
return defaultStream, nil
}

func getOSImageStreams(mc machineconfigclient.Interface, dc dynamic.Interface) (OSImageStreams, error) {
var osImageStreams OSImageStreams
var errs []error

osImageStream, err := mc.MachineconfigurationV1alpha1().OSImageStreams().Get(context.TODO(), "cluster", metav1.GetOptions{})
// TODO @pablintino MCO-2320. Remove this call to platformidentification.GetOSImageStreamDefaultStream
// and replace it with a bare machineConfigClient.MachineconfigurationV1().OSImageStreams().Get()
defaultStream, err := GetOSImageStreamDefaultStream(dc)
if err != nil {
if !kapierrs.IsNotFound(err) {
errs = append(errs, fmt.Errorf("error getting OSImageStream singleton: %v", err))
}
errs = append(errs, err)
} else {
osImageStreams.Default = osImageStream.Status.DefaultStream
osImageStreams.Default = defaultStream
}

mcpList, err := mc.MachineconfigurationV1().MachineConfigPools().List(context.TODO(), metav1.ListOptions{})
Expand Down
159 changes: 74 additions & 85 deletions pkg/monitortestlibrary/platformidentification/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ import (
"testing"

mcapiv1 "github.com/openshift/api/machineconfiguration/v1"
mcapiv1alpha1 "github.com/openshift/api/machineconfiguration/v1alpha1"
machineconfigclient "github.com/openshift/client-go/machineconfiguration/clientset/versioned"
mcv1 "github.com/openshift/client-go/machineconfiguration/clientset/versioned/typed/machineconfiguration/v1"
mcv1alpha1 "github.com/openshift/client-go/machineconfiguration/clientset/versioned/typed/machineconfiguration/v1alpha1"
kapierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
)

// Minimal fakes: embed the real interfaces (nil-valued) and override only
Expand All @@ -22,16 +22,12 @@ import (

type fakeMCClient struct {
machineconfigclient.Interface
v1 *fakeMCv1
v1alpha1 *fakeMCv1alpha1
v1 *fakeMCv1
}

func (f *fakeMCClient) MachineconfigurationV1() mcv1.MachineconfigurationV1Interface {
return f.v1
}
func (f *fakeMCClient) MachineconfigurationV1alpha1() mcv1alpha1.MachineconfigurationV1alpha1Interface {
return f.v1alpha1
}

type fakeMCv1 struct {
mcv1.MachineconfigurationV1Interface
Expand All @@ -42,15 +38,6 @@ func (f *fakeMCv1) MachineConfigPools() mcv1.MachineConfigPoolInterface {
return f.pools
}

type fakeMCv1alpha1 struct {
mcv1alpha1.MachineconfigurationV1alpha1Interface
streams *fakeOSImageStreams
}

func (f *fakeMCv1alpha1) OSImageStreams() mcv1alpha1.OSImageStreamInterface {
return f.streams
}

type fakeMCPools struct {
mcv1.MachineConfigPoolInterface
list *mcapiv1.MachineConfigPoolList
Expand All @@ -61,27 +48,34 @@ func (f *fakeMCPools) List(_ context.Context, _ metav1.ListOptions) (*mcapiv1.Ma
return f.list, f.err
}

type fakeOSImageStreams struct {
mcv1alpha1.OSImageStreamInterface
obj *mcapiv1alpha1.OSImageStream
err error
}

func (f *fakeOSImageStreams) Get(_ context.Context, _ string, _ metav1.GetOptions) (*mcapiv1alpha1.OSImageStream, error) {
return f.obj, f.err
}

func newFakeMCClient(osImageStream *mcapiv1alpha1.OSImageStream, osImageStreamErr error, mcpList *mcapiv1.MachineConfigPoolList, mcpErr error) *fakeMCClient {
func newFakeMCClient(mcpList *mcapiv1.MachineConfigPoolList, mcpErr error) *fakeMCClient {
return &fakeMCClient{
v1: &fakeMCv1{
pools: &fakeMCPools{list: mcpList, err: mcpErr},
},
v1alpha1: &fakeMCv1alpha1{
streams: &fakeOSImageStreams{obj: osImageStream, err: osImageStreamErr},
},
}
}

type fakeDynClient struct {
dynamic.Interface
obj *unstructured.Unstructured
err error
}

func (f *fakeDynClient) Resource(_ schema.GroupVersionResource) dynamic.NamespaceableResourceInterface {
return &fakeDynResource{obj: f.obj, err: f.err}
}

type fakeDynResource struct {
dynamic.NamespaceableResourceInterface
obj *unstructured.Unstructured
err error
}

func (f *fakeDynResource) Get(_ context.Context, _ string, _ metav1.GetOptions, _ ...string) (*unstructured.Unstructured, error) {
return f.obj, f.err
}

func mcp(name, osImageStreamName string) mcapiv1.MachineConfigPool {
return mcapiv1.MachineConfigPool{
ObjectMeta: metav1.ObjectMeta{Name: name},
Expand All @@ -95,11 +89,17 @@ func mcpList(pools ...mcapiv1.MachineConfigPool) *mcapiv1.MachineConfigPoolList
return &mcapiv1.MachineConfigPoolList{Items: pools}
}

func osImageStreamSingleton(defaultStream string) *mcapiv1alpha1.OSImageStream {
return &mcapiv1alpha1.OSImageStream{
ObjectMeta: metav1.ObjectMeta{Name: "cluster"},
Status: mcapiv1alpha1.OSImageStreamStatus{
DefaultStream: defaultStream,
func unstructuredOSImageStream(defaultStream string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "machineconfiguration.openshift.io/v1",
"kind": "OSImageStream",
"metadata": map[string]interface{}{
"name": "cluster",
},
"status": map[string]interface{}{
"defaultStream": defaultStream,
},
},
}
}
Expand All @@ -109,7 +109,8 @@ func TestGetOSImageStreams(t *testing.T) {

tests := []struct {
name string
client *fakeMCClient
mcClient *fakeMCClient
dynClient *fakeDynClient
wantDefault string
wantControlPlaneMachineConfigPool string
wantWorkerMachineConfigPool string
Expand All @@ -118,10 +119,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil bool
}{
{
name: "all streams populated with master and worker MCPs",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "all streams populated with master and worker MCPs",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-9.6"),
Expand All @@ -134,10 +134,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil: true,
},
{
name: "master and worker have different streams",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "master and worker have different streams",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-10.0"),
Expand All @@ -150,10 +149,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil: true,
},
{
name: "additional MCPs with unique stream names",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "additional MCPs with unique stream names",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-9.6"),
Expand All @@ -168,10 +166,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditional: []string{"rhel-10.0", "rhel-10.1"},
},
{
name: "additional MCPs with stream name matching default are deduplicated",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "additional MCPs with stream name matching default are deduplicated",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-9.6"),
Expand All @@ -185,10 +182,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil: true,
},
{
name: "additional MCPs with stream name matching master/worker are deduplicated",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "additional MCPs with stream name matching master/worker are deduplicated",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-10.0"),
mcp("worker", "rhel-10.1"),
Expand All @@ -204,10 +200,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditional: []string{"rhel-10.2"},
},
{
name: "OSImageStream singleton not found is not an error",
client: newFakeMCClient(
nil,
notFoundErr,
name: "OSImageStream singleton not found is not an error",
dynClient: &fakeDynClient{err: notFoundErr},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-9.6"),
Expand All @@ -219,10 +214,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil: true,
},
{
name: "OSImageStream singleton non-404 error is returned",
client: newFakeMCClient(
nil,
fmt.Errorf("internal server error"),
name: "OSImageStream singleton non-404 error is returned",
dynClient: &fakeDynClient{err: fmt.Errorf("internal server error")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-9.6"),
Expand All @@ -235,42 +229,38 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil: true,
},
{
name: "MCP list error is returned",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "MCP list error is returned",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
nil,
fmt.Errorf("failed to list MCPs"),
),
wantErr: true,
wantDefault: "rhel-9.6",
},
{
name: "both OSImageStream and MCP errors are joined",
client: newFakeMCClient(
nil,
fmt.Errorf("osimagestream error"),
name: "both OSImageStream and MCP errors are joined",
dynClient: &fakeDynClient{err: fmt.Errorf("osimagestream error")},
mcClient: newFakeMCClient(
nil,
fmt.Errorf("mcp error"),
),
wantErr: true,
},
{
name: "no MCPs returns empty streams",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "no MCPs returns empty streams",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(),
nil,
),
wantDefault: "rhel-9.6",
wantAdditionalNil: true,
},
{
name: "empty stream names on MCPs",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "empty stream names on MCPs",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", ""),
mcp("worker", ""),
Expand All @@ -281,10 +271,9 @@ func TestGetOSImageStreams(t *testing.T) {
wantAdditionalNil: true,
},
{
name: "additional MCPs with empty stream names are excluded",
client: newFakeMCClient(
osImageStreamSingleton("rhel-9.6"),
nil,
name: "additional MCPs with empty stream names are excluded",
dynClient: &fakeDynClient{obj: unstructuredOSImageStream("rhel-9.6")},
mcClient: newFakeMCClient(
mcpList(
mcp("master", "rhel-9.6"),
mcp("worker", "rhel-9.6"),
Expand All @@ -302,7 +291,7 @@ func TestGetOSImageStreams(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getOSImageStreams(tt.client)
got, err := getOSImageStreams(tt.mcClient, tt.dynClient)

if tt.wantErr && err == nil {
t.Fatal("expected error, got nil")
Expand Down
Loading