From d52e7df33a78dca49b4cfa5fbf4bdc816b094ab1 Mon Sep 17 00:00:00 2001 From: Jonathan Remy Date: Tue, 18 Nov 2025 09:23:32 +0100 Subject: [PATCH 1/3] feat(cockpit): add waiters for alerts --- api/cockpit/v1/cockpit_utils.go | 162 ++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 api/cockpit/v1/cockpit_utils.go diff --git a/api/cockpit/v1/cockpit_utils.go b/api/cockpit/v1/cockpit_utils.go new file mode 100644 index 00000000..00e2dfa0 --- /dev/null +++ b/api/cockpit/v1/cockpit_utils.go @@ -0,0 +1,162 @@ +package cockpit + +import ( + "time" + + "github.com/scaleway/scaleway-sdk-go/errors" + "github.com/scaleway/scaleway-sdk-go/internal/async" + "github.com/scaleway/scaleway-sdk-go/scw" +) + +const ( + defaultRetryInterval = 5 * time.Second + defaultTimeout = 5 * time.Minute +) + +// WaitForAlertRequest is used by WaitForAlert method. +type WaitForAlertRequest struct { + Region scw.Region + AlertID string + Timeout *time.Duration + RetryInterval *time.Duration +} + +// WaitForAlert waits for the alert to be in a "terminal state" before returning. +// This function can be used to wait for an alert to be enabled or disabled. +func (s *RegionalAPI) WaitForAlert(req *WaitForAlertRequest, opts ...scw.RequestOption) (*Alert, error) { + timeout := defaultTimeout + if req.Timeout != nil { + timeout = *req.Timeout + } + retryInterval := defaultRetryInterval + if req.RetryInterval != nil { + retryInterval = *req.RetryInterval + } + + terminalStatus := map[AlertStatus]struct{}{ + AlertStatusEnabled: {}, + AlertStatusDisabled: {}, + } + + alert, err := async.WaitSync(&async.WaitSyncConfig{ + Get: func() (any, bool, error) { + // List all alerts and find the one with matching ID + res, err := s.ListAlerts(&RegionalAPIListAlertsRequest{ + Region: req.Region, + }, opts...) + if err != nil { + return nil, false, err + } + + // Find the alert by ID + for _, alert := range res.Alerts { + if alert.ID == req.AlertID { + _, isTerminal := terminalStatus[alert.RuleStatus] + return alert, isTerminal, nil + } + } + + return nil, false, errors.New("alert not found") + }, + Timeout: timeout, + IntervalStrategy: async.LinearIntervalStrategy(retryInterval), + }) + if err != nil { + return nil, errors.Wrap(err, "waiting for alert failed") + } + return alert.(*Alert), nil +} + +// WaitForPreconfiguredAlertsRequest is used by WaitForPreconfiguredAlerts method. +type WaitForPreconfiguredAlertsRequest struct { + Region scw.Region + ProjectID string + PreconfiguredRules []string + TargetStatus AlertStatus + Timeout *time.Duration + RetryInterval *time.Duration +} + +// WaitForPreconfiguredAlerts waits for multiple preconfigured alerts to reach a target status. +// This function can be used to wait for alerts to be enabled or disabled after calling +// EnableAlertRules or DisableAlertRules. +func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlertsRequest, opts ...scw.RequestOption) ([]*Alert, error) { + timeout := defaultTimeout + if req.Timeout != nil { + timeout = *req.Timeout + } + retryInterval := defaultRetryInterval + if req.RetryInterval != nil { + retryInterval = *req.RetryInterval + } + + // For enabling/disabling, accept transitional states as terminal + var terminalStatus map[AlertStatus]struct{} + switch req.TargetStatus { + case AlertStatusEnabled: + // Accept both enabled and enabling as terminal states + terminalStatus = map[AlertStatus]struct{}{ + AlertStatusEnabled: {}, + AlertStatusEnabling: {}, + } + case AlertStatusDisabled: + // Accept both disabled and disabling as terminal states + terminalStatus = map[AlertStatus]struct{}{ + AlertStatusDisabled: {}, + AlertStatusDisabling: {}, + } + default: + terminalStatus = map[AlertStatus]struct{}{ + req.TargetStatus: {}, + } + } + + result, err := async.WaitSync(&async.WaitSyncConfig{ + Get: func() (any, bool, error) { + // List all preconfigured alerts for the project + res, err := s.ListAlerts(&RegionalAPIListAlertsRequest{ + Region: req.Region, + ProjectID: req.ProjectID, + IsPreconfigured: scw.BoolPtr(true), + }, opts...) + if err != nil { + return nil, false, err + } + + // Create a map of rule ID to alert + alertsByRuleID := make(map[string]*Alert) + for _, alert := range res.Alerts { + if alert.PreconfiguredData != nil && alert.PreconfiguredData.PreconfiguredRuleID != "" { + alertsByRuleID[alert.PreconfiguredData.PreconfiguredRuleID] = alert + } + } + + // Check if all requested alerts are in terminal state + var matchedAlerts []*Alert + for _, ruleID := range req.PreconfiguredRules { + alert, found := alertsByRuleID[ruleID] + if !found { + return nil, false, errors.New("preconfigured alert with rule ID %s not found", ruleID) + } + + _, isTerminal := terminalStatus[alert.RuleStatus] + if !isTerminal { + // At least one alert is not in terminal state, continue waiting + return nil, false, nil + } + + matchedAlerts = append(matchedAlerts, alert) + } + + // All alerts are in terminal state + return matchedAlerts, true, nil + }, + Timeout: timeout, + IntervalStrategy: async.LinearIntervalStrategy(retryInterval), + }) + if err != nil { + return nil, errors.Wrap(err, "waiting for preconfigured alerts failed") + } + return result.([]*Alert), nil +} + From 28470fd7dcfc01c16f8a484e94c1dda6bce8c7f4 Mon Sep 17 00:00:00 2001 From: Jonathan Remy Date: Tue, 18 Nov 2025 09:31:35 +0100 Subject: [PATCH 2/3] fix(cockpit): remove WaitForAlert and keep only WaitForPreconfiguredAlerts --- api/cockpit/v1/cockpit_utils.go | 63 --------------------------------- 1 file changed, 63 deletions(-) diff --git a/api/cockpit/v1/cockpit_utils.go b/api/cockpit/v1/cockpit_utils.go index 00e2dfa0..30c8a285 100644 --- a/api/cockpit/v1/cockpit_utils.go +++ b/api/cockpit/v1/cockpit_utils.go @@ -13,60 +13,6 @@ const ( defaultTimeout = 5 * time.Minute ) -// WaitForAlertRequest is used by WaitForAlert method. -type WaitForAlertRequest struct { - Region scw.Region - AlertID string - Timeout *time.Duration - RetryInterval *time.Duration -} - -// WaitForAlert waits for the alert to be in a "terminal state" before returning. -// This function can be used to wait for an alert to be enabled or disabled. -func (s *RegionalAPI) WaitForAlert(req *WaitForAlertRequest, opts ...scw.RequestOption) (*Alert, error) { - timeout := defaultTimeout - if req.Timeout != nil { - timeout = *req.Timeout - } - retryInterval := defaultRetryInterval - if req.RetryInterval != nil { - retryInterval = *req.RetryInterval - } - - terminalStatus := map[AlertStatus]struct{}{ - AlertStatusEnabled: {}, - AlertStatusDisabled: {}, - } - - alert, err := async.WaitSync(&async.WaitSyncConfig{ - Get: func() (any, bool, error) { - // List all alerts and find the one with matching ID - res, err := s.ListAlerts(&RegionalAPIListAlertsRequest{ - Region: req.Region, - }, opts...) - if err != nil { - return nil, false, err - } - - // Find the alert by ID - for _, alert := range res.Alerts { - if alert.ID == req.AlertID { - _, isTerminal := terminalStatus[alert.RuleStatus] - return alert, isTerminal, nil - } - } - - return nil, false, errors.New("alert not found") - }, - Timeout: timeout, - IntervalStrategy: async.LinearIntervalStrategy(retryInterval), - }) - if err != nil { - return nil, errors.Wrap(err, "waiting for alert failed") - } - return alert.(*Alert), nil -} - // WaitForPreconfiguredAlertsRequest is used by WaitForPreconfiguredAlerts method. type WaitForPreconfiguredAlertsRequest struct { Region scw.Region @@ -90,17 +36,14 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts retryInterval = *req.RetryInterval } - // For enabling/disabling, accept transitional states as terminal var terminalStatus map[AlertStatus]struct{} switch req.TargetStatus { case AlertStatusEnabled: - // Accept both enabled and enabling as terminal states terminalStatus = map[AlertStatus]struct{}{ AlertStatusEnabled: {}, AlertStatusEnabling: {}, } case AlertStatusDisabled: - // Accept both disabled and disabling as terminal states terminalStatus = map[AlertStatus]struct{}{ AlertStatusDisabled: {}, AlertStatusDisabling: {}, @@ -113,7 +56,6 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts result, err := async.WaitSync(&async.WaitSyncConfig{ Get: func() (any, bool, error) { - // List all preconfigured alerts for the project res, err := s.ListAlerts(&RegionalAPIListAlertsRequest{ Region: req.Region, ProjectID: req.ProjectID, @@ -123,7 +65,6 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts return nil, false, err } - // Create a map of rule ID to alert alertsByRuleID := make(map[string]*Alert) for _, alert := range res.Alerts { if alert.PreconfiguredData != nil && alert.PreconfiguredData.PreconfiguredRuleID != "" { @@ -131,7 +72,6 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts } } - // Check if all requested alerts are in terminal state var matchedAlerts []*Alert for _, ruleID := range req.PreconfiguredRules { alert, found := alertsByRuleID[ruleID] @@ -141,14 +81,12 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts _, isTerminal := terminalStatus[alert.RuleStatus] if !isTerminal { - // At least one alert is not in terminal state, continue waiting return nil, false, nil } matchedAlerts = append(matchedAlerts, alert) } - // All alerts are in terminal state return matchedAlerts, true, nil }, Timeout: timeout, @@ -159,4 +97,3 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts } return result.([]*Alert), nil } - From be5ed6571ead9f467e9820cc9b1c810057d46d2d Mon Sep 17 00:00:00 2001 From: Jonathan Remy Date: Thu, 20 Nov 2025 16:01:10 +0100 Subject: [PATCH 3/3] fix(cockpit): harden wait for alerts --- api/cockpit/v1/cockpit_utils.go | 48 +++++++++++++++++---------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/api/cockpit/v1/cockpit_utils.go b/api/cockpit/v1/cockpit_utils.go index 30c8a285..4a828619 100644 --- a/api/cockpit/v1/cockpit_utils.go +++ b/api/cockpit/v1/cockpit_utils.go @@ -27,6 +27,14 @@ type WaitForPreconfiguredAlertsRequest struct { // This function can be used to wait for alerts to be enabled or disabled after calling // EnableAlertRules or DisableAlertRules. func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlertsRequest, opts ...scw.RequestOption) ([]*Alert, error) { + if req == nil { + return nil, errors.New("WaitForPreconfiguredAlertsRequest cannot be nil") + } + + if len(req.PreconfiguredRules) == 0 { + return []*Alert{}, nil + } + timeout := defaultTimeout if req.Timeout != nil { timeout = *req.Timeout @@ -36,23 +44,7 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts retryInterval = *req.RetryInterval } - var terminalStatus map[AlertStatus]struct{} - switch req.TargetStatus { - case AlertStatusEnabled: - terminalStatus = map[AlertStatus]struct{}{ - AlertStatusEnabled: {}, - AlertStatusEnabling: {}, - } - case AlertStatusDisabled: - terminalStatus = map[AlertStatus]struct{}{ - AlertStatusDisabled: {}, - AlertStatusDisabling: {}, - } - default: - terminalStatus = map[AlertStatus]struct{}{ - req.TargetStatus: {}, - } - } + listOpts := append([]scw.RequestOption{scw.WithAllPages()}, opts...) result, err := async.WaitSync(&async.WaitSyncConfig{ Get: func() (any, bool, error) { @@ -60,27 +52,26 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts Region: req.Region, ProjectID: req.ProjectID, IsPreconfigured: scw.BoolPtr(true), - }, opts...) + }, listOpts...) if err != nil { return nil, false, err } - alertsByRuleID := make(map[string]*Alert) + alertsByRuleID := make(map[string]*Alert, len(res.Alerts)) for _, alert := range res.Alerts { if alert.PreconfiguredData != nil && alert.PreconfiguredData.PreconfiguredRuleID != "" { alertsByRuleID[alert.PreconfiguredData.PreconfiguredRuleID] = alert } } - var matchedAlerts []*Alert + matchedAlerts := make([]*Alert, 0, len(req.PreconfiguredRules)) for _, ruleID := range req.PreconfiguredRules { alert, found := alertsByRuleID[ruleID] if !found { - return nil, false, errors.New("preconfigured alert with rule ID %s not found", ruleID) + return nil, false, nil } - _, isTerminal := terminalStatus[alert.RuleStatus] - if !isTerminal { + if !isAlertInTargetStatus(req.TargetStatus, alert.RuleStatus) { return nil, false, nil } @@ -97,3 +88,14 @@ func (s *RegionalAPI) WaitForPreconfiguredAlerts(req *WaitForPreconfiguredAlerts } return result.([]*Alert), nil } + +func isAlertInTargetStatus(target, current AlertStatus) bool { + switch target { + case AlertStatusEnabled: + return current == AlertStatusEnabled + case AlertStatusDisabled: + return current == AlertStatusDisabled + default: + return current == target + } +}