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
8 changes: 8 additions & 0 deletions api/v1alpha1/bmc_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,14 @@ type BMCStatus struct {
// +optional
LastResetTime *metav1.Time `json:"lastResetTime,omitempty"`

// MetricsReportSubscriptionLink is the link to the metrics report subscription of the bmc.
// +optional
MetricsReportSubscriptionLink string `json:"metricsReportSubscriptionLink,omitempty"`

// EventsSubscriptionLink is the link to the events subscription of the bmc.
// +optional
EventsSubscriptionLink string `json:"eventsSubscriptionLink,omitempty"`

// Conditions represents the latest available observations of the BMC's current state.
// +patchStrategy=merge
// +patchMergeKey=type
Expand Down
6 changes: 6 additions & 0 deletions bmc/bmc.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ type BMC interface {

// GetBMCUpgradeTask retrieves the task for the BMC upgrade.
GetBMCUpgradeTask(ctx context.Context, manufacturer string, taskURI string) (*redfish.Task, error)

// CreateEventSubscription creates an event subscription for the manager.q
CreateEventSubscription(ctx context.Context, destination string, eventType redfish.EventFormatType, protocol redfish.DeliveryRetryPolicy) (string, error)

// DeleteEventSubscription deletes an event subscription for the manager.
DeleteEventSubscription(ctx context.Context, uri string) error
Comment on lines +123 to +128
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor typo in comment.

Line 124 has a trailing 'q' in the comment: "manager.q" should be "manager."

🔎 Proposed fix
-	// CreateEventSubscription creates an event subscription for the manager.q
+	// CreateEventSubscription creates an event subscription for the manager.
 	CreateEventSubscription(ctx context.Context, destination string, eventType redfish.EventFormatType, protocol redfish.DeliveryRetryPolicy) (string, error)
📝 Committable suggestion

‼️ 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.

Suggested change
// CreateEventSubscription creates an event subscription for the manager.q
CreateEventSubscription(ctx context.Context, destination string, eventType redfish.EventFormatType, protocol redfish.DeliveryRetryPolicy) (string, error)
// DeleteEventSubscription deletes an event subscription for the manager.
DeleteEventSubscription(ctx context.Context, uri string) error
// CreateEventSubscription creates an event subscription for the manager.
CreateEventSubscription(ctx context.Context, destination string, eventType redfish.EventFormatType, protocol redfish.DeliveryRetryPolicy) (string, error)
// DeleteEventSubscription deletes an event subscription for the manager.
DeleteEventSubscription(ctx context.Context, uri string) error
🤖 Prompt for AI Agents
In bmc/bmc.go around lines 123 to 128, the comment for CreateEventSubscription
contains a stray trailing 'q' ("manager.q"); remove the 'q' so the comment reads
"manager." Update the comment text only, preserving punctuation and spacing.

}

type Entity struct {
Expand Down
3 changes: 2 additions & 1 deletion bmc/mock/server/data/Managers/BMC/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"Name": "Manager",
"ManagerType": "BMC",
"Description": "Contoso BMC",
"Manufacturer": "Contoso",
"ServiceEntryPointUUID": "92384634-2938-2342-8820-489239905423",
"UUID": "58893887-8974-2487-2389-841168418919",
"Model": "Joo Janta 200",
Expand Down Expand Up @@ -96,4 +97,4 @@
},
"@odata.id": "/redfish/v1/Managers/BMC",
"@Redfish.Copyright": "Copyright 2014-2023 DMTF. For the full DMTF copyright policy, see http://www.dmtf.org/about/policies/copyright."
}
}
109 changes: 107 additions & 2 deletions bmc/mock/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ var (
dataFS embed.FS
)

type Collection struct {
Members []Member `json:"Members"`
}

type Member struct {
OdataID string `json:"@odata.id"`
}

const (
LockedResourceState = "Locked"
UnlockedResourceState = "Unlocked"
Expand Down Expand Up @@ -81,6 +89,8 @@ func (s *MockServer) redfishHandler(w http.ResponseWriter, r *http.Request) {
s.handleRedfishPOST(w, r)
case http.MethodPatch:
s.handleRedfishPATCH(w, r)
case http.MethodDelete:
s.handleRedfishDelete(w, r)
default:
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
}
Expand Down Expand Up @@ -180,6 +190,47 @@ func resolvePath(urlPath string) string {
return path.Join("data", trimmed, "index.json")
}

func (s *MockServer) handleRedfishDelete(w http.ResponseWriter, r *http.Request) {
s.log.Info("Received request", "method", r.Method, "path", r.URL.Path)

urlPath := resolvePath(r.URL.Path)
s.mu.RLock()
defer s.mu.Unlock()
Comment on lines +197 to +198
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Lock mismatch causes deadlock.

s.mu.RLock() is paired with s.mu.Unlock() instead of s.mu.RUnlock(). This will cause a deadlock because RLock must be released with RUnlock, not Unlock.

Additionally, this function modifies s.overrides (lines 202, 230), which requires a write lock, not a read lock.

🔎 Proposed fix
 func (s *MockServer) handleRedfishDelete(w http.ResponseWriter, r *http.Request) {
 	s.log.Info("Received request", "method", r.Method, "path", r.URL.Path)

 	urlPath := resolvePath(r.URL.Path)
-	s.mu.RLock()
-	defer s.mu.Unlock()
+	s.mu.Lock()
+	defer s.mu.Unlock()
 	_, hasOverride := s.overrides[urlPath]
📝 Committable suggestion

‼️ 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.

Suggested change
s.mu.RLock()
defer s.mu.Unlock()
s.mu.Lock()
defer s.mu.Unlock()
🤖 Prompt for AI Agents
In bmc/mock/server/server.go around lines 197-198, the code currently calls
s.mu.RLock() but defers s.mu.Unlock(), which is a lock/unlock mismatch and also
insufficient because this function mutates s.overrides (lines ~202 and ~230).
Replace the read lock with a full write lock: call s.mu.Lock() and keep defer
s.mu.Unlock() so modifications are protected; if any read-only paths exist,
refactor them separately to use RLock/RUnlock, but for this function use
Lock/Unlock to fix the deadlock and ensure safe writes to s.overrides.

_, hasOverride := s.overrides[urlPath]
if hasOverride {
// remove the resource
delete(s.overrides, urlPath)
}
// get collection of the resource
collectionPath := path.Dir(urlPath)
cached, hasOverride := s.overrides[collectionPath]
var collection Collection
if hasOverride {
collection = cached.(Collection)
} else {
data, err := dataFS.ReadFile(collectionPath)
if err != nil {
http.NotFound(w, r)
return
}
if err := json.Unmarshal(data, &collection); err != nil {
http.Error(w, "Corrupt embedded JSON", http.StatusInternalServerError)
return
}
}
Comment on lines +206 to +220
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe type assertion may cause panic.

Line 209 performs a type assertion cached.(Collection) without an ok-check. If the cached value is not a Collection (e.g., it's a map[string]any), this will panic at runtime.

🔎 Proposed fix
 	cached, hasOverride := s.overrides[collectionPath]
 	var collection Collection
 	if hasOverride {
-		collection = cached.(Collection)
+		var ok bool
+		collection, ok = cached.(Collection)
+		if !ok {
+			http.Error(w, "Invalid collection type", http.StatusInternalServerError)
+			return
+		}
 	} else {

// remove member from collection
newMembers := make([]Member, 0)
for _, member := range collection.Members {
if member.OdataID != r.URL.Path {
newMembers = append(newMembers, member)
}
}
s.log.Info("Removing member from collection", "members", newMembers, "collection", collectionPath)
collection.Members = newMembers
s.overrides[collectionPath] = collection
w.WriteHeader(http.StatusNoContent)
}

func (s *MockServer) handleRedfishGET(w http.ResponseWriter, r *http.Request) {
urlPath := resolvePath(r.URL.Path)

Expand Down Expand Up @@ -239,9 +290,15 @@ func (s *MockServer) handleRedfishPOST(w http.ResponseWriter, r *http.Request) {
}
}(r.Body)

s.log.Info("POST body received", "body", string(body))
var update map[string]any
if err := json.Unmarshal(body, &update); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}

s.log.Info("POST body received", "body", string(body))
urlPath := resolvePath(r.URL.Path)

switch {
case strings.Contains(urlPath, "Actions/ComputerSystem.Reset"):
// Simulate a system reset action
Expand Down Expand Up @@ -304,7 +361,55 @@ func (s *MockServer) handleRedfishPOST(w http.ResponseWriter, r *http.Request) {
case strings.Contains(urlPath, "UpdateService/Actions/UpdateService.SimpleUpdate"):
// Simulate a firmware update action
default:
s.log.Info("Unhandled POST request", "path", urlPath)
// Handle resource creation in collections
s.mu.Lock()
defer s.mu.Unlock()
cached, hasOverride := s.overrides[urlPath]
var base Collection
if hasOverride {
s.log.Info("Using overridden data for POST", "path", urlPath)
base = cached.(Collection)
} else {
s.log.Info("Using embedded data for POST", "path", urlPath)
data, err := dataFS.ReadFile(urlPath)
if err != nil {
s.log.Error(err, "Failed to read embedded data for POST", "path", urlPath)
http.NotFound(w, r)
return
}
if err := json.Unmarshal(data, &base); err != nil {
http.Error(w, "Corrupt embedded JSON", http.StatusInternalServerError)
return
}
}
Comment on lines +369 to +384
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe type assertion may cause panic.

Line 371 performs cached.(Collection) without an ok-check. This can panic if the cached value is a different type (e.g., map[string]any).

🔎 Proposed fix
 	if hasOverride {
 		s.log.Info("Using overridden data for POST", "path", urlPath)
-		base = cached.(Collection)
+		var ok bool
+		base, ok = cached.(Collection)
+		if !ok {
+			http.Error(w, "Resource is not a collection", http.StatusBadRequest)
+			return
+		}
 	} else {
📝 Committable suggestion

‼️ 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.

Suggested change
if hasOverride {
s.log.Info("Using overridden data for POST", "path", urlPath)
base = cached.(Collection)
} else {
s.log.Info("Using embedded data for POST", "path", urlPath)
data, err := dataFS.ReadFile(urlPath)
if err != nil {
s.log.Error(err, "Failed to read embedded data for POST", "path", urlPath)
http.NotFound(w, r)
return
}
if err := json.Unmarshal(data, &base); err != nil {
http.Error(w, "Corrupt embedded JSON", http.StatusInternalServerError)
return
}
}
if hasOverride {
s.log.Info("Using overridden data for POST", "path", urlPath)
var ok bool
base, ok = cached.(Collection)
if !ok {
http.Error(w, "Resource is not a collection", http.StatusBadRequest)
return
}
} else {
s.log.Info("Using embedded data for POST", "path", urlPath)
data, err := dataFS.ReadFile(urlPath)
if err != nil {
s.log.Error(err, "Failed to read embedded data for POST", "path", urlPath)
http.NotFound(w, r)
return
}
if err := json.Unmarshal(data, &base); err != nil {
http.Error(w, "Corrupt embedded JSON", http.StatusInternalServerError)
return
}
}
🤖 Prompt for AI Agents
In bmc/mock/server/server.go around lines 369 to 384, the code uses a type
assertion cached.(Collection) without an ok-check which can panic if cached
holds a different type; modify this to perform the comma-ok form (v, ok :=
cached.(Collection)), and if ok is true use v as base, otherwise log an error
(including type information), avoid panicking and either fall back to reading
the embedded data or return an HTTP error response (e.g., 500 or 404) depending
on desired behavior; ensure any error path returns after writing the HTTP
response so execution doesn't continue with an invalid base.

// If resource collection (has "Members"), add a new member
if len(base.Members) > 0 {
newID := fmt.Sprintf("%d", len(base.Members)+1)
location := path.Join(r.URL.Path, newID)
newMemberPath := resolvePath(location)
base.Members = append(base.Members, Member{
OdataID: location,
})
s.log.Info("Adding new member", "id", newID, "location", location, "memberPath", newMemberPath)
if strings.HasSuffix(r.URL.Path, "/Subscriptions") {
w.Header().Set("Location", location)
}
s.overrides[urlPath] = base
s.overrides[newMemberPath] = update
} else {
base.Members = make([]Member, 0)
location := r.URL.JoinPath("1").String()
base.Members = []Member{
{
OdataID: r.URL.JoinPath("1").String(),
},
}
s.overrides[urlPath] = base
if strings.HasSuffix(r.URL.Path, "/Subscriptions") {
w.Header().Set("Location", location)
}
}
Comment on lines +399 to +411
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resource data not stored for empty collections.

When the collection is initially empty (line 399 else branch), the code stores the collection with the new member (line 407) but does not store the update data for the new resource itself. Compare with lines 397-398 in the non-empty case where both are stored.

🔎 Proposed fix
 	} else {
-		base.Members = make([]Member, 0)
 		location := r.URL.JoinPath("1").String()
+		newMemberPath := resolvePath(location)
 		base.Members = []Member{
 			{
 				OdataID: r.URL.JoinPath("1").String(),
 			},
 		}
 		s.overrides[urlPath] = base
+		s.overrides[newMemberPath] = update
 		if strings.HasSuffix(r.URL.Path, "/Subscriptions") {
 			w.Header().Set("Location", location)
 		}
 	}
📝 Committable suggestion

‼️ 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.

Suggested change
} else {
base.Members = make([]Member, 0)
location := r.URL.JoinPath("1").String()
base.Members = []Member{
{
OdataID: r.URL.JoinPath("1").String(),
},
}
s.overrides[urlPath] = base
if strings.HasSuffix(r.URL.Path, "/Subscriptions") {
w.Header().Set("Location", location)
}
}
} else {
location := r.URL.JoinPath("1").String()
newMemberPath := resolvePath(location)
base.Members = []Member{
{
OdataID: r.URL.JoinPath("1").String(),
},
}
s.overrides[urlPath] = base
s.overrides[newMemberPath] = update
if strings.HasSuffix(r.URL.Path, "/Subscriptions") {
w.Header().Set("Location", location)
}
}
🤖 Prompt for AI Agents
In bmc/mock/server/server.go around lines 399 to 411, when the collection is
empty the code creates and stores the collection with the new Member but fails
to store the resource's update data for the new member; add the same storage
step used in the non-empty branch by writing the update payload for the new
member URL (r.URL.JoinPath("1").String()) into s.overrides (or the appropriate
map) so the newly created resource has its data persisted, and keep setting the
Location header for Subscriptions as already done.

s.log.Info("Storing updated data for POST", "path", urlPath, "data", update)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
_, err = w.Write([]byte(`{"status": "created"}`))
Expand Down
78 changes: 78 additions & 0 deletions bmc/redfish.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"slices"
"strings"
"time"
Expand Down Expand Up @@ -996,3 +997,80 @@ func (r *RedfishBMC) GetBMCUpgradeTask(ctx context.Context, manufacturer string,

return oem.GetTaskMonitorDetails(ctx, respTask)
}

type subscriptionPayload struct {
Destination string `json:"Destination,omitempty"`
EventTypes []redfish.EventType `json:"EventTypes,omitempty"`
EventFormatType redfish.EventFormatType `json:"EventFormatType,omitempty"`
RegistryPrefixes []string `json:"RegistryPrefixes,omitempty"`
ResourceTypes []string `json:"ResourceTypes,omitempty"`
DeliveryRetryPolicy redfish.DeliveryRetryPolicy `json:"DeliveryRetryPolicy,omitempty"`
HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"`
Oem interface{} `json:"Oem,omitempty"`
Protocol redfish.EventDestinationProtocol `json:"Protocol,omitempty"`
Context string `json:"Context,omitempty"`
}

func (r *RedfishBMC) CreateEventSubscription(
ctx context.Context,
destination string,
eventFormatType redfish.EventFormatType,
retry redfish.DeliveryRetryPolicy,
) (string, error) {
service := r.client.GetService()
ev, err := service.EventService()
if err != nil {
return "", fmt.Errorf("failed to get event service: %w", err)
}
if !ev.ServiceEnabled {
return "", fmt.Errorf("event service is not enabled")
}
payload := &subscriptionPayload{
Destination: destination,
EventFormatType: eventFormatType, // event or metricreport
Protocol: redfish.RedfishEventDestinationProtocol,
DeliveryRetryPolicy: retry,
Context: "metal3-operator",
}
client := ev.GetClient()
// some implementations (like Dell) do not support ResourceTypes and RegistryPrefixes
if len(ev.ResourceTypes) == 0 {
payload.EventTypes = []redfish.EventType{}
} else {
payload.RegistryPrefixes = []string{""} // Filters by the prefix of the event's MessageId, which points to a Message Registry: [Base, ResourceEvent, iLOEvents]
payload.ResourceTypes = []string{""} // Filters by the schema name (Resource Type) of the event's OriginOfCondition: [Chassis, ComputerSystem, Power]
}
resp, err := client.Post(ev.Subscriptions, payload)
if err != nil {
return "", err
}
// return subscription link from returned location
subscriptionLink := resp.Header.Get("Location")
urlParser, err := url.ParseRequestURI(subscriptionLink)
if err == nil {
subscriptionLink = urlParser.RequestURI()
}
return subscriptionLink, nil
Comment on lines +1037 to +1053
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Resource leak: HTTP response body not closed.

The response from client.Post (line 1043) is never closed, causing a resource leak. HTTP response bodies must be closed even when only reading headers.

Also, setting RegistryPrefixes and ResourceTypes to arrays containing empty strings ([]string{""}) seems incorrect and may cause unexpected filtering behavior on the BMC.

🔎 Suggested fix
 	resp, err := client.Post(ev.Subscriptions, payload)
 	if err != nil {
 		return "", err
 	}
+	defer resp.Body.Close()
 	// return subscription link from returned location
 	subscriptionLink := resp.Header.Get("Location")

For the empty strings issue, consider either omitting these fields or providing meaningful filter values:

-	if len(ev.ResourceTypes) == 0 {
-		payload.EventTypes = []redfish.EventType{}
-	} else {
-		payload.RegistryPrefixes = []string{""} // Filters by the prefix of the event's MessageId
-		payload.ResourceTypes = []string{""}    // Filters by the schema name
-	}
+	// Omit ResourceTypes and RegistryPrefixes to receive all events
+	// Some implementations (like Dell) do not support these fields
📝 Committable suggestion

‼️ 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.

Suggested change
if len(ev.ResourceTypes) == 0 {
payload.EventTypes = []redfish.EventType{}
} else {
payload.RegistryPrefixes = []string{""} // Filters by the prefix of the event's MessageId, which points to a Message Registry: [Base, ResourceEvent, iLOEvents]
payload.ResourceTypes = []string{""} // Filters by the schema name (Resource Type) of the event's OriginOfCondition: [Chassis, ComputerSystem, Power]
}
resp, err := client.Post(ev.Subscriptions, payload)
if err != nil {
return "", err
}
// return subscription link from returned location
subscriptionLink := resp.Header.Get("Location")
urlParser, err := url.ParseRequestURI(subscriptionLink)
if err == nil {
subscriptionLink = urlParser.RequestURI()
}
return subscriptionLink, nil
// Omit ResourceTypes and RegistryPrefixes to receive all events
// Some implementations (like Dell) do not support these fields
resp, err := client.Post(ev.Subscriptions, payload)
if err != nil {
return "", err
}
defer resp.Body.Close()
// return subscription link from returned location
subscriptionLink := resp.Header.Get("Location")
urlParser, err := url.ParseRequestURI(subscriptionLink)
if err == nil {
subscriptionLink = urlParser.RequestURI()
}
return subscriptionLink, nil
🤖 Prompt for AI Agents
In bmc/redfish.go around lines 1037-1053, the HTTP response from client.Post is
never closed (resource leak) and RegistryPrefixes/ResourceTypes are set to
[]string{""} which is incorrect; fix by: after calling client.Post, if err !=
nil and resp != nil close resp.Body before returning the error, and when err ==
nil defer resp.Body.Close() immediately; also stop assigning empty-string
slices—only set payload.RegistryPrefixes and payload.ResourceTypes when you have
meaningful non-empty values, otherwise leave them unset/nil so no unintended
filtering occurs.

}

func (r RedfishBMC) DeleteEventSubscription(ctx context.Context, uri string) error {
service := r.client.GetService()
ev, err := service.EventService()
if err != nil {
return fmt.Errorf("failed to get event service: %w", err)
}
if !ev.ServiceEnabled {
return fmt.Errorf("event service is not enabled")
}
event, err := ev.GetEventSubscription(uri)
if err != nil {
return fmt.Errorf("failed to get event subscription: %w", err)
}
if event == nil {
return nil
}
if err := ev.DeleteEventSubscription(uri); err != nil {
return fmt.Errorf("failed to delete event subscription: %w", err)
}
return nil
}
Comment on lines +1056 to +1076
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Value receiver on DeleteEventSubscription is inconsistent with other methods.

All other methods on RedfishBMC use pointer receivers (func (r *RedfishBMC)), but DeleteEventSubscription uses a value receiver (func (r RedfishBMC)). This is inconsistent and could cause subtle issues if the struct is large or contains mutable state.

🔎 Suggested fix
-func (r RedfishBMC) DeleteEventSubscription(ctx context.Context, uri string) error {
+func (r *RedfishBMC) DeleteEventSubscription(ctx context.Context, uri string) error {
📝 Committable suggestion

‼️ 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.

Suggested change
func (r RedfishBMC) DeleteEventSubscription(ctx context.Context, uri string) error {
service := r.client.GetService()
ev, err := service.EventService()
if err != nil {
return fmt.Errorf("failed to get event service: %w", err)
}
if !ev.ServiceEnabled {
return fmt.Errorf("event service is not enabled")
}
event, err := ev.GetEventSubscription(uri)
if err != nil {
return fmt.Errorf("failed to get event subscription: %w", err)
}
if event == nil {
return nil
}
if err := ev.DeleteEventSubscription(uri); err != nil {
return fmt.Errorf("failed to delete event subscription: %w", err)
}
return nil
}
func (r *RedfishBMC) DeleteEventSubscription(ctx context.Context, uri string) error {
service := r.client.GetService()
ev, err := service.EventService()
if err != nil {
return fmt.Errorf("failed to get event service: %w", err)
}
if !ev.ServiceEnabled {
return fmt.Errorf("event service is not enabled")
}
event, err := ev.GetEventSubscription(uri)
if err != nil {
return fmt.Errorf("failed to get event subscription: %w", err)
}
if event == nil {
return nil
}
if err := ev.DeleteEventSubscription(uri); err != nil {
return fmt.Errorf("failed to delete event subscription: %w", err)
}
return nil
}
🤖 Prompt for AI Agents
In bmc/redfish.go around lines 1056 to 1076, the method DeleteEventSubscription
uses a value receiver (func (r RedfishBMC)) while all other RedfishBMC methods
use pointer receivers; change the method signature to use a pointer receiver
(func (r *RedfishBMC)) to match the rest of the type, avoid copying the struct,
and ensure any calls or interfaces expecting the pointer type still compile
(update any references/imports/tests if necessary).

29 changes: 29 additions & 0 deletions cmd/manager/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"time"

"github.com/ironcore-dev/controller-utils/conditionutils"
"github.com/ironcore-dev/metal-operator/internal/serverevents"
webhookmetalv1alpha1 "github.com/ironcore-dev/metal-operator/internal/webhook/v1alpha1"
"sigs.k8s.io/controller-runtime/pkg/manager"

Expand Down Expand Up @@ -73,6 +74,9 @@ func main() { // nolint: gocyclo
registryPort int
registryProtocol string
registryURL string
eventPort int
eventURL string
eventProtocol string
registryResyncInterval time.Duration
webhookPort int
enforceFirstBoot bool
Expand Down Expand Up @@ -118,6 +122,10 @@ func main() { // nolint: gocyclo
flag.StringVar(&registryURL, "registry-url", "", "The URL of the registry.")
flag.StringVar(&registryProtocol, "registry-protocol", "http", "The protocol to use for the registry.")
flag.IntVar(&registryPort, "registry-port", 10000, "The port to use for the registry.")
flag.StringVar(&eventURL, "event-url", "", "The URL of the server events endpoint for alerts and metrics.")
flag.IntVar(&eventPort, "event-port", 10001, "The port to use for the server events endpoint for alerts and metrics.")
flag.StringVar(&eventProtocol, "event-protocol", "http",
"The protocol to use for the server events endpoint for alerts and metrics.")
flag.StringVar(&probeImage, "probe-image", "", "Image for the first boot probing of a Server.")
flag.StringVar(&probeOSImage, "probe-os-image", "", "OS image for the first boot probing of a Server.")
flag.StringVar(&managerNamespace, "manager-namespace", "default", "Namespace the manager is running in.")
Expand Down Expand Up @@ -189,6 +197,15 @@ func main() { // nolint: gocyclo
registryURL = fmt.Sprintf("%s://%s:%d", registryProtocol, registryAddr, registryPort)
}

// set the correct event URL by getting the address from the environment
var eventAddr string
if eventURL == "" {
eventAddr = os.Getenv("EVENT_ADDRESS")
if eventAddr != "" {
eventURL = fmt.Sprintf("%s://%s:%d", eventProtocol, eventAddr, eventPort)
}
}
Comment on lines +200 to +207
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Logic bug: event server won't start when --event-url is provided directly.

If the user provides --event-url directly via the command line flag, eventAddr remains empty, and the condition at line 575 (if eventAddr != "") will be false, preventing the event server from starting.

The condition should check eventURL instead of eventAddr.

🔎 Suggested fix
-	if eventAddr != "" {
+	if eventURL != "" {
 		setupLog.Info("starting event server for alerts and metrics", "EventURL", eventURL)

Also applies to: 575-584

🤖 Prompt for AI Agents
In cmd/manager/main.go around lines 200 to 207, the conditional uses eventAddr
to decide whether to build eventURL which breaks when the user passed
--event-url directly; change the check to test eventURL (i.e., if eventURL == ""
then populate from ENV, else leave it) and when constructing the URL ensure you
use the appropriate source; apply the same fix to the similar block around lines
575 to 584 (replace checks of eventAddr with checks of eventURL and only fall
back to ENV when eventURL is empty).


// if the enable-http2 flag is false (the default), http/2 should be disabled
// due to its vulnerabilities. More specifically, disabling http/2 will
// prevent from being vulnerable to the HTTP/2 Stream Cancelation and
Expand Down Expand Up @@ -329,6 +346,7 @@ func main() { // nolint: gocyclo
BMCResetWaitTime: bmcResetWaitingInterval,
BMCClientRetryInterval: bmcResetResyncInterval,
ManagerNamespace: managerNamespace,
EventURL: eventURL,
Conditions: conditionutils.NewAccessor(conditionutils.AccessorOptions{}),
BMCOptions: bmc.Options{
BasicAuth: true,
Expand Down Expand Up @@ -554,6 +572,17 @@ func main() { // nolint: gocyclo
os.Exit(1)
}

if eventAddr != "" {
setupLog.Info("starting event server for alerts and metrics", "EventURL", eventURL)
eventServer := serverevents.NewServer(setupLog, fmt.Sprintf(":%d", eventPort))
go func() {
if err := eventServer.Start(ctx); err != nil {
setupLog.Error(err, "problem running event server")
os.Exit(1)
}
}()
}
Comment on lines +575 to +584
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Inconsistent lifecycle management: event server started via goroutine instead of manager runnable.

The registry server (lines 562-573) is added as a manager.RunnableFunc, ensuring proper lifecycle management tied to the manager. However, the event server is started in a raw goroutine with os.Exit(1) inside it.

Issues:

  1. os.Exit(1) in a goroutine prevents proper cleanup and can cause race conditions.
  2. The event server won't stop gracefully when the manager stops, unlike the registry server.
🔎 Suggested fix: use manager runnable pattern
 	if eventAddr != "" {
-		setupLog.Info("starting event server for alerts and metrics", "EventURL", eventURL)
-		eventServer := serverevents.NewServer(setupLog, fmt.Sprintf(":%d", eventPort))
-		go func() {
-			if err := eventServer.Start(ctx); err != nil {
-				setupLog.Error(err, "problem running event server")
-				os.Exit(1)
-			}
-		}()
+		if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
+			setupLog.Info("starting event server for alerts and metrics", "EventURL", eventURL)
+			eventServer := serverevents.NewServer(setupLog, fmt.Sprintf(":%d", eventPort))
+			if err := eventServer.Start(ctx); err != nil {
+				return fmt.Errorf("unable to start event server: %w", err)
+			}
+			<-ctx.Done()
+			return nil
+		})); err != nil {
+			setupLog.Error(err, "unable to add event server runnable to manager")
+			os.Exit(1)
+		}
 	}
📝 Committable suggestion

‼️ 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.

Suggested change
if eventAddr != "" {
setupLog.Info("starting event server for alerts and metrics", "EventURL", eventURL)
eventServer := serverevents.NewServer(setupLog, fmt.Sprintf(":%d", eventPort))
go func() {
if err := eventServer.Start(ctx); err != nil {
setupLog.Error(err, "problem running event server")
os.Exit(1)
}
}()
}
if eventAddr != "" {
if err := mgr.Add(manager.RunnableFunc(func(ctx context.Context) error {
setupLog.Info("starting event server for alerts and metrics", "EventURL", eventURL)
eventServer := serverevents.NewServer(setupLog, fmt.Sprintf(":%d", eventPort))
if err := eventServer.Start(ctx); err != nil {
return fmt.Errorf("unable to start event server: %w", err)
}
<-ctx.Done()
return nil
})); err != nil {
setupLog.Error(err, "unable to add event server runnable to manager")
os.Exit(1)
}
}
🤖 Prompt for AI Agents
In cmd/manager/main.go around lines 575-584, the event server is started in a
raw goroutine and calls os.Exit on error which bypasses graceful shutdown and
differs from the registry server's manager.Runnable lifecycle. Replace the
goroutine with a manager.Runnable (e.g., manager.Add(manager.RunnableFunc(...)))
that starts eventServer.Start(ctx) and returns any error instead of calling
os.Exit, and ensure the runnable handles context cancellation to stop the server
gracefully (call eventServer.Stop or similar on ctx.Done()) so the event server
lifecycle is tied to the manager and cleans up properly.


setupLog.Info("starting manager")
if err := mgr.Start(ctx); err != nil {
setupLog.Error(err, "problem running manager")
Expand Down
8 changes: 8 additions & 0 deletions config/crd/bases/metal.ironcore.dev_bmcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ spec:
- type
type: object
type: array
eventsSubscriptionLink:
description: EventsSubscriptionLink is the link to the events subscription
of the bmc.
type: string
firmwareVersion:
description: FirmwareVersion is the version of the firmware currently
running on the BMC.
Expand All @@ -278,6 +282,10 @@ spec:
manufacturer:
description: Manufacturer is the name of the BMC manufacturer.
type: string
metricsReportSubscriptionLink:
description: MetricsReportSubscriptionLink is the link to the metrics
report subscription of the bmc.
type: string
model:
description: Model is the model number or name of the BMC.
type: string
Expand Down
8 changes: 8 additions & 0 deletions dist/chart/templates/crd/metal.ironcore.dev_bmcs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,10 @@ spec:
- type
type: object
type: array
eventsSubscriptionLink:
description: EventsSubscriptionLink is the link to the events subscription
of the bmc.
type: string
firmwareVersion:
description: FirmwareVersion is the version of the firmware currently
running on the BMC.
Expand All @@ -284,6 +288,10 @@ spec:
manufacturer:
description: Manufacturer is the name of the BMC manufacturer.
type: string
metricsReportSubscriptionLink:
description: MetricsReportSubscriptionLink is the link to the metrics
report subscription of the bmc.
type: string
model:
description: Model is the model number or name of the BMC.
type: string
Expand Down
Loading
Loading