diff --git a/bundle/manifests/oran-o2ims.clusterserviceversion.yaml b/bundle/manifests/oran-o2ims.clusterserviceversion.yaml index ba5ce2ad..8ff3bd78 100644 --- a/bundle/manifests/oran-o2ims.clusterserviceversion.yaml +++ b/bundle/manifests/oran-o2ims.clusterserviceversion.yaml @@ -1102,16 +1102,33 @@ spec: verbs: - create - post + - nonResourceURLs: + - /o2ims-infrastructureCluster/v1/alarmDictionaries + verbs: + - get + - list + - nonResourceURLs: + - /o2ims-infrastructureCluster/v1/alarmDictionaries/* + verbs: + - get - nonResourceURLs: - /o2ims-infrastructureCluster/v1/nodeClusterTypes verbs: - get - list + - nonResourceURLs: + - /o2ims-infrastructureCluster/v1/nodeClusterTypes/* + verbs: + - get - nonResourceURLs: - /o2ims-infrastructureCluster/v1/nodeClusters verbs: - get - list + - nonResourceURLs: + - /o2ims-infrastructureCluster/v1/nodeClusters/* + verbs: + - get - apiGroups: - "" resources: diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index 369924e4..76fc533d 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -14,16 +14,33 @@ rules: verbs: - create - post +- nonResourceURLs: + - /o2ims-infrastructureCluster/v1/alarmDictionaries + verbs: + - get + - list +- nonResourceURLs: + - /o2ims-infrastructureCluster/v1/alarmDictionaries/* + verbs: + - get - nonResourceURLs: - /o2ims-infrastructureCluster/v1/nodeClusterTypes verbs: - get - list +- nonResourceURLs: + - /o2ims-infrastructureCluster/v1/nodeClusterTypes/* + verbs: + - get - nonResourceURLs: - /o2ims-infrastructureCluster/v1/nodeClusters verbs: - get - list +- nonResourceURLs: + - /o2ims-infrastructureCluster/v1/nodeClusters/* + verbs: + - get - apiGroups: - "" resources: diff --git a/internal/controllers/inventory_controller.go b/internal/controllers/inventory_controller.go index 5899cfd3..f9dcbd0e 100644 --- a/internal/controllers/inventory_controller.go +++ b/internal/controllers/inventory_controller.go @@ -75,6 +75,10 @@ import ( //+kubebuilder:rbac:urls="/internal/v1/caas-alerts/alertmanager",verbs=create;post //+kubebuilder:rbac:urls="/o2ims-infrastructureCluster/v1/nodeClusterTypes",verbs=get;list //+kubebuilder:rbac:urls="/o2ims-infrastructureCluster/v1/nodeClusters",verbs=get;list +//+kubebuilder:rbac:urls="/o2ims-infrastructureCluster/v1/alarmDictionaries",verbs=get;list +//+kubebuilder:rbac:urls="/o2ims-infrastructureCluster/v1/nodeClusterTypes/*",verbs=get +//+kubebuilder:rbac:urls="/o2ims-infrastructureCluster/v1/nodeClusters/*",verbs=get +//+kubebuilder:rbac:urls="/o2ims-infrastructureCluster/v1/alarmDictionaries/*",verbs=get //+kubebuilder:rbac:urls="/hardware-manager/inventory/*",verbs=get;list //+kubebuilder:rbac:groups="batch",resources=cronjobs,verbs=get;list;watch;create;update;patch;delete @@ -1248,12 +1252,23 @@ func (t *reconcilerTask) createAlarmServerClusterRole(ctx context.Context) error NonResourceURLs: []string{ "/o2ims-infrastructureCluster/v1/nodeClusterTypes", "/o2ims-infrastructureCluster/v1/nodeClusters", + "/o2ims-infrastructureCluster/v1/alarmDictionaries", }, Verbs: []string{ "get", "list", }, }, + { + NonResourceURLs: []string{ + "/o2ims-infrastructureCluster/v1/nodeClusterTypes/*", + "/o2ims-infrastructureCluster/v1/nodeClusters/*", + "/o2ims-infrastructureCluster/v1/alarmDictionaries/*", + }, + Verbs: []string{ + "get", + }, + }, }, } diff --git a/internal/controllers/utils/constants.go b/internal/controllers/utils/constants.go index 34a9c252..646af484 100644 --- a/internal/controllers/utils/constants.go +++ b/internal/controllers/utils/constants.go @@ -289,6 +289,9 @@ const ( ClusterTemplateArtifactsLabel = "clustertemplates.o2ims.provisioning.oran.org/templateId" ) +// AlarmDefinitionSeverityField severity field within additional fields of alarm definition +const AlarmDefinitionSeverityField = "severity" + // Alertmanager values const ( AlertmanagerObjectName = "alertmanager" diff --git a/internal/service/alarms/api/server.go b/internal/service/alarms/api/server.go index 4a35c4c5..fcdc57dd 100644 --- a/internal/service/alarms/api/server.go +++ b/internal/service/alarms/api/server.go @@ -9,8 +9,6 @@ import ( "sync" "time" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/serviceconfig" - "github.com/google/uuid" "github.com/jackc/pgerrcode" "github.com/jackc/pgx/v5/pgconn" @@ -20,7 +18,7 @@ import ( "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/models" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/repo" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver" + "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/serviceconfig" api2 "github.com/openshift-kni/oran-o2ims/internal/service/common/api" common "github.com/openshift-kni/oran-o2ims/internal/service/common/api/generated" "github.com/openshift-kni/oran-o2ims/internal/service/common/notifier" @@ -308,44 +306,16 @@ func (a *AlarmsServer) PatchAlarm(ctx context.Context, request api.PatchAlarmReq }), nil } - // Check if associated alarm definition has clearing type "manual". If not, return 409. - alarmDefinition, err := a.AlarmsRepository.GetAlarmDefinition(ctx, *record.AlarmDefinitionID) - if errors.Is(err, utils.ErrNotFound) { - return api.PatchAlarm404ApplicationProblemPlusJSONResponse(common.ProblemDetails{ - AdditionalAttributes: &map[string]string{ - "alarmEventRecordId": request.AlarmEventRecordId.String(), - }, - Detail: "associated Alarm Definition not found", - Status: http.StatusNotFound, - }), nil - } - - if alarmDefinition.ClearingType != string(common.MANUAL) { - return api.PatchAlarm409ApplicationProblemPlusJSONResponse(common.ProblemDetails{ - AdditionalAttributes: &map[string]string{ - "alarmEventRecordId": request.AlarmEventRecordId.String(), - }, - Detail: "cannot clear an alarm with clearing type other than MANUAL", - Status: http.StatusConflict, - }), nil - } + // All our alarms have AUTOMATIC clearing type + // TODO: support clearing type MANUAL alarms - // Check if the Alarm Event Record has already been cleared - if record.PerceivedSeverity == perceivedSeverity { - // Nothing to patch - return api.PatchAlarm409ApplicationProblemPlusJSONResponse(common.ProblemDetails{ - AdditionalAttributes: &map[string]string{ - "alarmEventRecordId": request.AlarmEventRecordId.String(), - }, - Detail: "Alarm record is already cleared", - Status: http.StatusConflict, - }), nil - } - - // Patch the Alarm Event Record - record.PerceivedSeverity = perceivedSeverity - currentTime := time.Now() - record.AlarmClearedTime = ¤tTime + return api.PatchAlarm409ApplicationProblemPlusJSONResponse(common.ProblemDetails{ + AdditionalAttributes: &map[string]string{ + "alarmEventRecordId": request.AlarmEventRecordId.String(), + }, + Detail: "cannot clear an alarm with clearing type other than MANUAL", + Status: http.StatusConflict, + }), nil } // Patch alarmAcknowledged @@ -515,25 +485,16 @@ func (a *AlarmsServer) AmNotification(ctx context.Context, request api.AmNotific return nil, fmt.Errorf("%s: %w", msg, err) } - // Get NodeCluster NodeClusterType mapping - var clusterIDToNodeClusterTypeID map[uuid.UUID]uuid.UUID + // Get cached cluster server data + var clusterServer infrastructure.Client for i := range a.Infrastructure.Clients { - if a.Infrastructure.Clients[i].Name() == clusterserver.Name { - clusterIDToNodeClusterTypeID = a.Infrastructure.Clients[i].(*clusterserver.ClusterServer).GetClusterIDToResourceTypeID() - break + if a.Infrastructure.Clients[i].Name() == infrastructure.Name { + clusterServer = a.Infrastructure.Clients[i] } } - // Get the definition data based on current set of Alert names and managed cluster ID - alarmDefinitions, err := a.AlarmsRepository.GetAlarmDefinitions(ctx, request.Body, clusterIDToNodeClusterTypeID) - if err != nil { - msg := "failed to get AlarmDefinitions" - slog.Error(msg, "error", err) - return nil, fmt.Errorf("%s: %w", msg, err) - } - // Combine possible definitions with events - aerModels := alertmanager.ConvertAmToAlarmEventRecordModels(request.Body, alarmDefinitions, clusterIDToNodeClusterTypeID) + aerModels := alertmanager.ConvertAmToAlarmEventRecordModels(request.Body, clusterServer) // Insert and update AlarmEventRecord if err := a.AlarmsRepository.UpsertAlarmEventRecord(ctx, aerModels); err != nil { diff --git a/internal/service/alarms/internal/alertmanager/alertmanager.go b/internal/service/alarms/internal/alertmanager/alertmanager.go index dc39fab2..bcfa6180 100644 --- a/internal/service/alarms/internal/alertmanager/alertmanager.go +++ b/internal/service/alarms/internal/alertmanager/alertmanager.go @@ -11,13 +11,13 @@ import ( "time" "github.com/google/uuid" - api "github.com/openshift-kni/oran-o2ims/internal/service/alarms/api/generated" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/models" - corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/openshift-kni/oran-o2ims/internal/controllers/utils" + api "github.com/openshift-kni/oran-o2ims/internal/service/alarms/api/generated" + "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/models" + "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure" "github.com/openshift-kni/oran-o2ims/internal/service/common/clients/k8s" ) @@ -79,7 +79,7 @@ func Setup(ctx context.Context) error { } // ConvertAmToAlarmEventRecordModels get alarmEventRecords based on the alertmanager notification and AlarmDefinition -func ConvertAmToAlarmEventRecordModels(am *api.AlertmanagerNotification, aDefinitionRecords []models.AlarmDefinition, clusterIDToObjectTypeID map[uuid.UUID]uuid.UUID) []models.AlarmEventRecord { +func ConvertAmToAlarmEventRecordModels(am *api.AlertmanagerNotification, infrastructureClient infrastructure.Client) []models.AlarmEventRecord { records := make([]models.AlarmEventRecord, 0, len(am.Alerts)) for _, alert := range am.Alerts { record := models.AlarmEventRecord{ @@ -89,16 +89,6 @@ func ConvertAmToAlarmEventRecordModels(am *api.AlertmanagerNotification, aDefini Fingerprint: *alert.Fingerprint, } - // for caas alerts object is the cluster ID - record.ObjectID = GetClusterID(*alert.Labels) - - // derive ObjectTypeID from ObjectID - if id := record.ObjectID; id != nil { - if typeID, exists := clusterIDToObjectTypeID[*id]; exists { - record.ObjectTypeID = &typeID - } - } - // Make sure the current payload has the right severity if *alert.Status == api.Resolved { record.PerceivedSeverity = severityToPerceivedSeverity("cleared") @@ -110,11 +100,27 @@ func ConvertAmToAlarmEventRecordModels(am *api.AlertmanagerNotification, aDefini // Update Extensions with things we didn't really process record.Extensions = getExtensions(*alert.Labels, *alert.Annotations) + // for caas alerts object is the cluster ID + record.ObjectID = GetClusterID(*alert.Labels) + + // derive ObjectTypeID from ObjectID + if record.ObjectID != nil { + objectTypeID, err := infrastructureClient.GetObjectTypeID(*record.ObjectID) + if err != nil { + slog.Warn("Could not get object type ID", "objectID", record.ObjectID, "err", err.Error()) + } else { + record.ObjectTypeID = &objectTypeID + } + } + // See if possible to pick up additional info from its definition - for _, def := range aDefinitionRecords { - if def.AlarmName == GetAlertName(*alert.Labels) && def.ObjectTypeID == *record.ObjectTypeID && severityToPerceivedSeverity(def.Severity) == record.PerceivedSeverity { - record.AlarmDefinitionID = &def.AlarmDefinitionID - record.ProbableCauseID = &def.ProbableCauseID + if record.ObjectTypeID != nil { + _, severity := GetPerceivedSeverity(*alert.Labels) + alarmDefinitionID, err := infrastructureClient.GetAlarmDefinitionID(*record.ObjectTypeID, GetAlertName(*alert.Labels), severity) + if err != nil { + slog.Warn("Could not get alarm definition ID", "objectTypeID", *record.ObjectTypeID, "name", GetAlertName(*alert.Labels), "severity", severity, "err", err.Error()) + } else { + record.AlarmDefinitionID = &alarmDefinitionID } } diff --git a/internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.down.sql b/internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.down.sql deleted file mode 100644 index 1436abce..00000000 --- a/internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.down.sql +++ /dev/null @@ -1,8 +0,0 @@ --- Drop the trigger that updates updated_at for alarm_dictionary -DROP TRIGGER IF EXISTS set_alarm_dictionary_timestamp ON alarm_dictionary; - --- Drop the function used by the alarm_dictionary trigger -DROP FUNCTION IF EXISTS update_alarm_dictionary_timestamp; - --- Drop the alarm_dictionary table -DROP TABLE IF EXISTS alarm_dictionary; diff --git a/internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.up.sql b/internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.up.sql deleted file mode 100644 index bfef8b5a..00000000 --- a/internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.up.sql +++ /dev/null @@ -1,31 +0,0 @@ --- Holds information about each unique object type (ResourceType or NodeClusterType) and its associated dictionary details -CREATE TABLE IF NOT EXISTS alarm_dictionary ( - -- O-RAN - alarm_dictionary_version VARCHAR(50) NOT NULL, -- Version of the alarm dictionary, potentially in major.minor format - alarm_dictionary_schema_version VARCHAR(50) DEFAULT 'TBD-O-RAN-DEFINED' NOT NULL, -- Schema version, defaulted to TBD-O-RAN-DEFINED - entity_type VARCHAR(255) NOT NULL, -- Combination of objectType.model and objectType.version - vendor VARCHAR(255) NOT NULL, -- objectType.vendor field - management_interface_id VARCHAR(50)[] DEFAULT ARRAY['O2IMS']::VARCHAR[], -- Management interfaces, defaults to o2ims - pk_notification_field TEXT[] DEFAULT ARRAY['alarm_dictionary_id']::TEXT[], -- Primary key notification field, defaults to alarm_dictionary_id - - -- Internal - alarm_dictionary_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Unique identifier for each alarm dictionary - object_type_id UUID NOT NULL UNIQUE, -- One-to-one relation between a object type (ResourceType or NodeClusterType) and alarmDictionary - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, -- Record creation timestamp - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -- Record last update timestamp -); - --- Trigger function to update updated_at timestamp for alarm_dictionary -CREATE OR REPLACE FUNCTION update_alarm_dictionary_timestamp() - RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Trigger to execute update_alarm_dictionary_timestamp before each update on alarm_dictionary -CREATE OR REPLACE TRIGGER set_alarm_dictionary_timestamp - BEFORE UPDATE ON alarm_dictionary - FOR EACH ROW - EXECUTE FUNCTION update_alarm_dictionary_timestamp(); diff --git a/internal/service/alarms/internal/db/migrations/000003_create_alarm_event_record_table.down.sql b/internal/service/alarms/internal/db/migrations/000001_create_alarm_event_record_table.down.sql similarity index 100% rename from internal/service/alarms/internal/db/migrations/000003_create_alarm_event_record_table.down.sql rename to internal/service/alarms/internal/db/migrations/000001_create_alarm_event_record_table.down.sql diff --git a/internal/service/alarms/internal/db/migrations/000003_create_alarm_event_record_table.up.sql b/internal/service/alarms/internal/db/migrations/000001_create_alarm_event_record_table.up.sql similarity index 100% rename from internal/service/alarms/internal/db/migrations/000003_create_alarm_event_record_table.up.sql rename to internal/service/alarms/internal/db/migrations/000001_create_alarm_event_record_table.up.sql diff --git a/internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.down.sql b/internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.down.sql deleted file mode 100644 index d288e208..00000000 --- a/internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.down.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Drop the trigger for setting object_type_id in alarm_definition -DROP TRIGGER IF EXISTS populate_alarm_definition_object_type_id ON alarm_definition; - --- Drop the trigger function for setting object_type_id in alarm_definition -DROP FUNCTION IF EXISTS set_alarm_definition_object_type_id; - --- Drop the trigger for updating updated_at in alarm_definition -DROP TRIGGER IF EXISTS set_alarm_definition_updated_at ON alarm_definition; - --- Drop the trigger function for updating updated_at in alarm_definition -DROP FUNCTION IF EXISTS update_alarm_definition_timestamp; - --- Drop the alarm_definition table -DROP TABLE IF EXISTS alarm_definition; diff --git a/internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.up.sql b/internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.up.sql deleted file mode 100644 index 4432e60f..00000000 --- a/internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.up.sql +++ /dev/null @@ -1,65 +0,0 @@ --- Defines specific alarms associated with a object type (ResourceType or NodeClusterType), referencing alarm_dictionary -CREATE TABLE IF NOT EXISTS alarm_definition ( - -- O-RAN - alarm_definition_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Unique identifier for each alarm - alarm_name VARCHAR(255) NOT NULL, -- Short name for the alarm - alarm_last_change VARCHAR(50) NOT NULL, -- Version in which this alarm last changed. Can use alarmDict version - alarm_change_type VARCHAR(20) DEFAULT 'ADDED' NOT NULL, -- Type of change (ADDED, DELETED, MODIFIED) - alarm_description TEXT NOT NULL, -- For caas it's rules[].summary and rules[].description - proposed_repair_actions TEXT NOT NULL, -- For caas it's rules[].runbook_url - clearing_type VARCHAR(20) DEFAULT 'AUTOMATIC' NOT NULL, -- Clearing type (AUTOMATIC or MANUAL) - management_interface_id VARCHAR(20)[] DEFAULT ARRAY['O2IMS']::VARCHAR[], -- Use default - pk_notification_field TEXT[] DEFAULT ARRAY['alarmDefinitionID']::TEXT[], -- Use default - alarm_additional_fields JSONB, -- In case of caas alerts, add anything that we didnt read from annotations or labels of the rules. Any data from PrometheusRule CR (where the rules are) may also dumped here. - - -- Internal - alarm_dictionary_id UUID NOT NULL, -- Foreign key to alarm_dictionary to create a one-to-many relationship - object_type_id UUID NOT NULL, -- Duplicate for faster querying, and this will be auto added with trigger. During runtime (for caas) we are only guaranteed to get name and managed_cluster_id. - probable_cause_id UUID DEFAULT gen_random_uuid(), -- Embedding this here to simplify schema. If we user asks for pc (directly or through event) we simply return the row with pc_id, alarm_name and alarm_description. - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, -- Record creation timestamp, Auto - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, -- Record last update timestamp, Auto - - -- Internal rule properties - -- There exists alerts within the same PrometheusRule.Group that have the same name but different severity label. - -- By adding this columns and a unique constraint on (object_type_id, alarm_name, severity), we can differentiate between them. - -- All the Alerts from the Core Platform Monitoring have a severity label (except alert Watchdog). Alerts without a severity label are not affected by this. - severity VARCHAR(20) NOT NULL, -- severity of the alarm, obtained from severity label - - -- Constraints - CONSTRAINT unique_alarm UNIQUE(object_type_id, alarm_name, severity), -- This is what uniquely identifies an alarm - CONSTRAINT fk_alarm_dictionary FOREIGN KEY (alarm_dictionary_id) REFERENCES alarm_dictionary(alarm_dictionary_id) ON DELETE CASCADE, -- Delete auto - CONSTRAINT chk_alarm_change_type CHECK (alarm_change_type IN ('ADDED', 'DELETED', 'MODIFIED')), - CONSTRAINT chk_clearing_type CHECK (clearing_type IN ('AUTOMATIC', 'MANUAL')) -); - - --- Trigger function to set object_type_id in alarm_definition -CREATE OR REPLACE FUNCTION set_alarm_definition_object_type_id() - RETURNS TRIGGER AS $$ -BEGIN - -- Set object_type_id based on the associated alarm_dictionary_id - NEW.object_type_id := (SELECT object_type_id FROM alarm_dictionary WHERE alarm_dictionary_id = NEW.alarm_dictionary_id); - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Trigger to populate object_type_id -CREATE OR REPLACE TRIGGER populate_alarm_definition_object_type_id - BEFORE INSERT OR UPDATE ON alarm_definition - FOR EACH ROW - EXECUTE FUNCTION set_alarm_definition_object_type_id(); - --- Trigger function to update updated_at on row updates for alarm_definition -CREATE OR REPLACE FUNCTION update_alarm_definition_timestamp() - RETURNS TRIGGER AS $$ -BEGIN - NEW.updated_at = CURRENT_TIMESTAMP; - RETURN NEW; -END; -$$ LANGUAGE plpgsql; - --- Trigger to execute update_alarm_definition_timestamp before each update on alarm_definition -CREATE OR REPLACE TRIGGER set_alarm_definition_updated_at - BEFORE UPDATE ON alarm_definition - FOR EACH ROW - EXECUTE FUNCTION update_alarm_definition_timestamp(); diff --git a/internal/service/alarms/internal/db/migrations/000004_create_alarm_subscription_info_table.down.sql b/internal/service/alarms/internal/db/migrations/000002_create_alarm_subscription_info_table.down.sql similarity index 100% rename from internal/service/alarms/internal/db/migrations/000004_create_alarm_subscription_info_table.down.sql rename to internal/service/alarms/internal/db/migrations/000002_create_alarm_subscription_info_table.down.sql diff --git a/internal/service/alarms/internal/db/migrations/000004_create_alarm_subscription_info_table.up.sql b/internal/service/alarms/internal/db/migrations/000002_create_alarm_subscription_info_table.up.sql similarity index 100% rename from internal/service/alarms/internal/db/migrations/000004_create_alarm_subscription_info_table.up.sql rename to internal/service/alarms/internal/db/migrations/000002_create_alarm_subscription_info_table.up.sql diff --git a/internal/service/alarms/internal/db/migrations/000005_create_alarm_service_configuration_table.down.sql b/internal/service/alarms/internal/db/migrations/000003_create_alarm_service_configuration_table.down.sql similarity index 100% rename from internal/service/alarms/internal/db/migrations/000005_create_alarm_service_configuration_table.down.sql rename to internal/service/alarms/internal/db/migrations/000003_create_alarm_service_configuration_table.down.sql diff --git a/internal/service/alarms/internal/db/migrations/000005_create_alarm_service_configuration_table.up.sql b/internal/service/alarms/internal/db/migrations/000003_create_alarm_service_configuration_table.up.sql similarity index 100% rename from internal/service/alarms/internal/db/migrations/000005_create_alarm_service_configuration_table.up.sql rename to internal/service/alarms/internal/db/migrations/000003_create_alarm_service_configuration_table.up.sql diff --git a/internal/service/alarms/internal/db/models/alarm_definition.go b/internal/service/alarms/internal/db/models/alarm_definition.go deleted file mode 100644 index e5316f2a..00000000 --- a/internal/service/alarms/internal/db/models/alarm_definition.go +++ /dev/null @@ -1,45 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -// AlarmDefinition represents the alarm_definition table in the database -type AlarmDefinition struct { - AlarmDefinitionID uuid.UUID `db:"alarm_definition_id"` - - AlarmName string `db:"alarm_name"` - AlarmLastChange string `db:"alarm_last_change"` - AlarmChangeType string `db:"alarm_change_type"` - AlarmDescription string `db:"alarm_description"` - ProposedRepairActions string `db:"proposed_repair_actions"` - ClearingType string `db:"clearing_type"` - ManagementInterfaceID []string `db:"management_interface_id"` - PKNotificationField []string `db:"pk_notification_field"` - AlarmAdditionalFields map[string]string `db:"alarm_additional_fields"` - - AlarmDictionaryID uuid.UUID `db:"alarm_dictionary_id"` - ObjectTypeID uuid.UUID `db:"object_type_id"` - ProbableCauseID uuid.UUID `db:"probable_cause_id"` - Severity string `db:"severity"` - - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} - -// TableName returns the name of the table in the database -func (r AlarmDefinition) TableName() string { - return "alarm_definition" -} - -// PrimaryKey returns the primary key of the table -func (r AlarmDefinition) PrimaryKey() string { - return "alarm_definition_id" -} - -// OnConflict returns the column or constraint to be used in the UPSERT operation -func (r AlarmDefinition) OnConflict() string { - return "unique_alarm" -} diff --git a/internal/service/alarms/internal/db/models/alarm_dictionary.go b/internal/service/alarms/internal/db/models/alarm_dictionary.go deleted file mode 100644 index 60bdd61a..00000000 --- a/internal/service/alarms/internal/db/models/alarm_dictionary.go +++ /dev/null @@ -1,38 +0,0 @@ -package models - -import ( - "time" - - "github.com/google/uuid" -) - -// AlarmDictionary represents the alarm_dictionary table in the database -type AlarmDictionary struct { - AlarmDictionaryID uuid.UUID `db:"alarm_dictionary_id"` - - AlarmDictionaryVersion string `db:"alarm_dictionary_version"` - AlarmDictionarySchemaVersion string `db:"alarm_dictionary_schema_version"` - EntityType string `db:"entity_type"` - Vendor string `db:"vendor"` - ManagementInterfaceID []string `db:"management_interface_id"` - PKNotificationField []string `db:"pk_notification_field"` - - ObjectTypeID uuid.UUID `db:"object_type_id"` - CreatedAt time.Time `db:"created_at"` - UpdatedAt time.Time `db:"updated_at"` -} - -// TableName returns the name of the table in the database -func (r AlarmDictionary) TableName() string { - return "alarm_dictionary" -} - -// PrimaryKey returns the primary key of the table -func (r AlarmDictionary) PrimaryKey() string { - return "alarm_dictionary_id" -} - -// OnConflict returns the column or constraint to be used in the UPSERT operation -func (r AlarmDictionary) OnConflict() string { - return "object_type_id" -} diff --git a/internal/service/alarms/internal/db/repo/alarms_repository.go b/internal/service/alarms/internal/db/repo/alarms_repository.go index a46ec929..34c0b165 100644 --- a/internal/service/alarms/internal/db/repo/alarms_repository.go +++ b/internal/service/alarms/internal/db/repo/alarms_repository.go @@ -7,8 +7,6 @@ import ( "time" "github.com/google/uuid" - api "github.com/openshift-kni/oran-o2ims/internal/service/alarms/api/generated" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/alertmanager" "github.com/stephenafamo/bob" "github.com/stephenafamo/bob/dialect/psql" "github.com/stephenafamo/bob/dialect/psql/dialect" @@ -16,6 +14,7 @@ import ( "github.com/stephenafamo/bob/dialect/psql/sm" "github.com/stephenafamo/bob/dialect/psql/um" + api "github.com/openshift-kni/oran-o2ims/internal/service/alarms/api/generated" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/models" "github.com/openshift-kni/oran-o2ims/internal/service/common/utils" ) @@ -111,90 +110,6 @@ func (ar *AlarmsRepository) GetAlarmSubscription(ctx context.Context, id uuid.UU return utils.Find[models.AlarmSubscription](ctx, ar.Db, id) } -// DeleteAlarmDictionariesNotIn deletes all alarm dictionaries that are not in the list of object type IDs -func (ar *AlarmsRepository) DeleteAlarmDictionariesNotIn(ctx context.Context, ids []any) error { - tags := utils.GetDBTagsFromStructFields(models.AlarmDictionary{}, "ObjectTypeID") - - expr := psql.Quote(tags["ObjectTypeID"]).NotIn(psql.Arg(ids...)) - _, err := utils.Delete[models.AlarmDictionary](ctx, ar.Db, expr) - return err -} - -// GetAlarmDefinition grabs a row of alarm_definition using a primary key -func (ar *AlarmsRepository) GetAlarmDefinition(ctx context.Context, id uuid.UUID) (*models.AlarmDefinition, error) { - return utils.Find[models.AlarmDefinition](ctx, ar.Db, id) -} - -// DeleteAlarmDefinitionsNotIn deletes all alarm definitions identified by the primary key that are not in the list of IDs. -// The Where expression also uses the column "object_type_id" to filter the records -func (ar *AlarmsRepository) DeleteAlarmDefinitionsNotIn(ctx context.Context, ids []any, objectTypeID uuid.UUID) (int64, error) { - tags := utils.GetDBTagsFromStructFields(models.AlarmDefinition{}, "ObjectTypeID") - - expr := psql.Quote(models.AlarmDefinition{}.PrimaryKey()).NotIn(psql.Arg(ids...)).And(psql.Quote(tags["ObjectTypeID"]).EQ(psql.Arg(objectTypeID))) - count, err := utils.Delete[models.AlarmDefinition](ctx, ar.Db, expr) - return count, err -} - -// UpsertAlarmDictionary inserts or updates an alarm dictionary record -func (ar *AlarmsRepository) UpsertAlarmDictionary(ctx context.Context, record models.AlarmDictionary) ([]models.AlarmDictionary, error) { - dbModel := models.AlarmDictionary{} - columns := utils.GetColumns(dbModel, []string{"AlarmDictionaryVersion", "EntityType", "Vendor", "ObjectTypeID"}) - - query := psql.Insert( - im.Into(record.TableName(), columns...), - im.Values(psql.Arg(record.AlarmDictionaryVersion, record.EntityType, record.Vendor, record.ObjectTypeID)), - im.OnConflict(record.OnConflict()).DoUpdate( - im.SetExcluded(columns...)), - im.Returning(record.PrimaryKey()), - ) - - sql, args, err := query.Build() - if err != nil { - return nil, fmt.Errorf("failed to build query: %w", err) - } - - return utils.ExecuteCollectRows[models.AlarmDictionary](ctx, ar.Db, sql, args) -} - -// UpsertAlarmDefinitions inserts or updates alarm definition records -func (ar *AlarmsRepository) UpsertAlarmDefinitions(ctx context.Context, records []models.AlarmDefinition) ([]models.AlarmDefinition, error) { - dbModel := models.AlarmDefinition{} - - if len(records) == 0 { - return []models.AlarmDefinition{}, nil - } - - columns := utils.GetColumns(records[0], []string{ - "AlarmName", "AlarmLastChange", "AlarmDescription", - "ProposedRepairActions", "AlarmAdditionalFields", "AlarmDictionaryID", - "Severity"}, - ) - - modInsert := []bob.Mod[*dialect.InsertQuery]{ - im.Into(dbModel.TableName(), columns...), - im.OnConflictOnConstraint(dbModel.OnConflict()).DoUpdate( - im.SetExcluded(columns...)), - im.Returning(dbModel.PrimaryKey()), - } - - for _, record := range records { - modInsert = append(modInsert, im.Values(psql.Arg(record.AlarmName, record.AlarmLastChange, record.AlarmDescription, - record.ProposedRepairActions, record.AlarmAdditionalFields, record.AlarmDictionaryID, - record.Severity))) - } - - query := psql.Insert( - modInsert..., - ) - - sql, args, err := query.Build() - if err != nil { - return nil, fmt.Errorf("failed to build query: %w", err) - } - - return utils.ExecuteCollectRows[models.AlarmDefinition](ctx, ar.Db, sql, args) -} - // UpsertAlarmEventRecord insert and updating an AlarmEventRecord. func (ar *AlarmsRepository) UpsertAlarmEventRecord(ctx context.Context, records []models.AlarmEventRecord) error { if len(records) == 0 { @@ -259,80 +174,6 @@ func buildAlarmEventRecordUpsertQuery(records []models.AlarmEventRecord) (string return query.Build() //nolint:wrapcheck } -// GetAlarmDefinitions needed to build out aer -func (ar *AlarmsRepository) GetAlarmDefinitions(ctx context.Context, am *api.AlertmanagerNotification, clusterIDToObjectTypeID map[uuid.UUID]uuid.UUID) ([]models.AlarmDefinition, error) { - if len(am.Alerts) == 0 { - slog.Warn("No events to retrieve corresponding definitions") - return []models.AlarmDefinition{}, nil // this should never happen - } - - // find all the keys needed to find corresponding definitions - keys := getGetAlertNameObjectTypeIDAndSeverity(am, clusterIDToObjectTypeID) - if len(keys) == 0 { - slog.Warn("No key found to retrieve definitions") - return []models.AlarmDefinition{}, nil - } - - if len(am.Alerts) != len(keys) { - slog.Warn("Could not find enough info from alerts to retrieve all corresponding definitions", - "missing_count", len(am.Alerts)-len(keys)) - } - - m := models.AlarmDefinition{} - dbTags := utils.GetAllDBTagsFromStruct(m) - query := psql.Select( - sm.Columns( - utils.GetColumnsAsAny(utils.GetColumns(m, []string{ - "AlarmName", "AlarmDefinitionID", "ProbableCauseID", - "ObjectTypeID", "Severity", - }))...), - sm.From(m.TableName()), - sm.Where( - psql.Group( - psql.Quote(dbTags["AlarmName"]), - psql.Quote(dbTags["ObjectTypeID"]), - psql.Quote(dbTags["Severity"]), - ). - In(keys...), - ), - ) - - sql, params, err := query.Build() - if err != nil { - return nil, fmt.Errorf("failed to build alarm definitions query when processing AM notification: %w", err) - } - - return utils.ExecuteCollectRows[models.AlarmDefinition](ctx, ar.Db, sql, params) -} - -func getGetAlertNameObjectTypeIDAndSeverity(am *api.AlertmanagerNotification, clusterIDToObjectTypeID map[uuid.UUID]uuid.UUID) []bob.Expression { - var b []bob.Expression - for _, alert := range am.Alerts { - if alert.Labels == nil { - continue - } - - labels := *alert.Labels - id := alertmanager.GetClusterID(labels) - if id == nil { - continue - } - - objectTypeId, ok := clusterIDToObjectTypeID[*id] - if !ok { - continue - } - - _, severity := alertmanager.GetPerceivedSeverity(labels) - b = append(b, psql.ArgGroup( - alertmanager.GetAlertName(labels), - objectTypeId, - severity, - )) - } - return b -} - // TimeNow allows test to override time.Now var TimeNow = time.Now diff --git a/internal/service/alarms/internal/db/repo/alarms_repository_interface.go b/internal/service/alarms/internal/db/repo/alarms_repository_interface.go index fd302f2d..5fecffab 100644 --- a/internal/service/alarms/internal/db/repo/alarms_repository_interface.go +++ b/internal/service/alarms/internal/db/repo/alarms_repository_interface.go @@ -21,13 +21,7 @@ type AlarmRepositoryInterface interface { DeleteAlarmSubscription(ctx context.Context, id uuid.UUID) (int64, error) CreateAlarmSubscription(ctx context.Context, record models.AlarmSubscription) (*models.AlarmSubscription, error) GetAlarmSubscription(ctx context.Context, id uuid.UUID) (*models.AlarmSubscription, error) - DeleteAlarmDictionariesNotIn(ctx context.Context, ids []any) error - GetAlarmDefinition(ctx context.Context, id uuid.UUID) (*models.AlarmDefinition, error) - DeleteAlarmDefinitionsNotIn(ctx context.Context, ids []any, objectTypeID uuid.UUID) (int64, error) - UpsertAlarmDictionary(ctx context.Context, record models.AlarmDictionary) ([]models.AlarmDictionary, error) - UpsertAlarmDefinitions(ctx context.Context, records []models.AlarmDefinition) ([]models.AlarmDefinition, error) UpsertAlarmEventRecord(ctx context.Context, records []models.AlarmEventRecord) error - GetAlarmDefinitions(ctx context.Context, am *api.AlertmanagerNotification, clusterMap map[uuid.UUID]uuid.UUID) ([]models.AlarmDefinition, error) ResolveNotificationIfNotInCurrent(ctx context.Context, am *api.AlertmanagerNotification) error GetAlarmsForSubscription(ctx context.Context, subscription models.AlarmSubscription) ([]models.AlarmEventRecord, error) UpdateSubscriptionEventCursor(ctx context.Context, subscription models.AlarmSubscription) error diff --git a/internal/service/alarms/internal/db/repo/alarms_repository_test.go b/internal/service/alarms/internal/db/repo/alarms_repository_test.go index 8ebccbe5..090cc4cf 100644 --- a/internal/service/alarms/internal/db/repo/alarms_repository_test.go +++ b/internal/service/alarms/internal/db/repo/alarms_repository_test.go @@ -139,55 +139,6 @@ var _ = Describe("AlarmsRepository", func() { }) }) - Describe("DeleteAlarmDefinitionsNotIn", func() { - When("deleting alarm definitions with a list of IDs to keep", func() { - It("deletes alarm definitions not in provided IDs with correct object type ID", func() { - ids := []any{uuid.New(), uuid.New()} - objectTypeID := uuid.New() - - mock.ExpectExec(fmt.Sprintf("DELETE FROM %s WHERE", models.AlarmDefinition{}.TableName())). - WithArgs(ids[0], ids[1], objectTypeID). - WillReturnResult(pgxmock.NewResult("DELETE", 2)) - - count, err := repo.DeleteAlarmDefinitionsNotIn(ctx, ids, objectTypeID) - Expect(err).NotTo(HaveOccurred()) - Expect(count).To(Equal(int64(2))) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - - When("the database operation fails", func() { - It("returns an error for deletion fail", func() { - ids := []any{uuid.New()} - objectTypeID := uuid.New() - - mock.ExpectExec(fmt.Sprintf("DELETE FROM %s WHERE", models.AlarmDefinition{}.TableName())). - WithArgs(ids[0], objectTypeID). - WillReturnError(fmt.Errorf("database error")) - - count, err := repo.DeleteAlarmDefinitionsNotIn(ctx, ids, objectTypeID) - Expect(err).To(HaveOccurred()) - Expect(count).To(Equal(int64(0))) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - - Context("when provided with an empty ID list", func() { - It("should execute successfully with no deletions", func() { - var ids []any - objectTypeID := uuid.New() - - mock.ExpectExec(fmt.Sprintf("DELETE FROM %s WHERE", models.AlarmDefinition{}.TableName())). - WithArgs(objectTypeID). - WillReturnResult(pgxmock.NewResult("DELETE", 0)) - - count, err := repo.DeleteAlarmDefinitionsNotIn(ctx, ids, objectTypeID) - Expect(err).NotTo(HaveOccurred()) - Expect(count).To(Equal(int64(0))) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - }) Describe("GetAlarmEventRecords", func() { When("records exist", func() { It("returns all alarm event records", func() { @@ -384,44 +335,6 @@ var _ = Describe("AlarmsRepository", func() { }) }) - Describe("UpsertAlarmDefinitions", func() { - When("upserting valid definitions", func() { - It("successfully upserts alarm definitions", func() { - defs := []models.AlarmDefinition{ - { - AlarmName: "test-alarm", - AlarmLastChange: "test", - Severity: string(api.AlarmSubscriptionInfoFilterNEW), - AlarmDictionaryID: uuid.New(), - }, - } - - mock.ExpectQuery(fmt.Sprintf("INSERT INTO %s", models.AlarmDefinition{}.TableName())). - WithArgs( - defs[0].AlarmName, defs[0].AlarmLastChange, - defs[0].AlarmDescription, defs[0].ProposedRepairActions, - defs[0].AlarmAdditionalFields, defs[0].AlarmDictionaryID, - defs[0].Severity, - ). - WillReturnRows(pgxmock.NewRows([]string{"alarm_definition_id"}).AddRow(uuid.New())) - - results, err := repo.UpsertAlarmDefinitions(ctx, defs) - Expect(err).NotTo(HaveOccurred()) - Expect(results).To(HaveLen(1)) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - - When("upserting empty input", func() { - It("handles empty input gracefully", func() { - results, err := repo.UpsertAlarmDefinitions(ctx, []models.AlarmDefinition{}) - Expect(err).NotTo(HaveOccurred()) - Expect(results).To(HaveLen(0)) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - }) - Describe("GetAlarmsForSubscription", func() { It("retrieves alarms based on subscription criteria", func() { f := api.AlarmSubscriptionInfoFilterNEW @@ -515,22 +428,6 @@ var _ = Describe("AlarmsRepository", func() { }) - Describe("DeleteAlarmDictionariesNotIn", func() { - When("there are dictionaries to delete", func() { - It("deletes alarm dictionaries not in provided IDs", func() { - ids := []any{uuid.New(), uuid.New()} - - mock.ExpectExec(fmt.Sprintf("DELETE FROM %s WHERE", models.AlarmDictionary{}.TableName())). - WithArgs(ids[0], ids[1]). - WillReturnResult(pgxmock.NewResult("DELETE", 1)) - - err := repo.DeleteAlarmDictionariesNotIn(ctx, ids) - Expect(err).NotTo(HaveOccurred()) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - }) - Describe("UpdateSubscriptionEventCursor", func() { When("update is successful", func() { It("updates subscription event cursor successfully", func() { @@ -567,33 +464,6 @@ var _ = Describe("AlarmsRepository", func() { }) }) - Describe("UpsertAlarmDictionary", func() { - When("upserting a new dictionary", func() { - It("successfully upserts alarm dictionary", func() { - record := models.AlarmDictionary{ - AlarmDictionaryVersion: "1.0", - EntityType: "TestEntity", - Vendor: "TestVendor", - ObjectTypeID: uuid.New(), - } - - mock.ExpectQuery(fmt.Sprintf("INSERT INTO %s", models.AlarmDictionary{}.TableName())). - WithArgs( - record.AlarmDictionaryVersion, - record.EntityType, - record.Vendor, - record.ObjectTypeID, - ). - WillReturnRows(pgxmock.NewRows([]string{"alarm_dictionary_id"}).AddRow(uuid.New())) - - results, err := repo.UpsertAlarmDictionary(ctx, record) - Expect(err).NotTo(HaveOccurred()) - Expect(results).To(HaveLen(1)) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - }) - Describe("ResolveNotificationIfNotInCurrent", func() { When("resolving notifications", func() { It("resolves notifications not in current payload", func() { @@ -622,103 +492,6 @@ var _ = Describe("AlarmsRepository", func() { }) }) - Describe("GetAlarmDefinition", func() { - When("patch is requested", func() { - It("retrieves alarm definition with ID", func() { - alertname := "CollectorNodeDown" //nolint:goconst - severity := "info" //nolint:goconst - - id := uuid.New() - mock.ExpectQuery(fmt.Sprintf("SELECT (.+) FROM %s WHERE", models.AlarmDefinition{}.TableName())). - WithArgs(id). - WillReturnRows(pgxmock.NewRows([]string{ - "alarm_name", "alarm_definition_id", "probable_cause_id", - "object_type_id", "severity", - }).AddRow( - alertname, uuid.New(), uuid.New(), - uuid.New(), severity, - )) - - result, err := repo.GetAlarmDefinition(ctx, id) - Expect(err).NotTo(HaveOccurred()) - Expect(result.AlarmName).To(Equal(alertname)) - Expect(result.Severity).To(Equal(severity)) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - }) - - Describe("GetAlarmDefinitions", func() { - When("valid alertmanager notification is provided", func() { - It("retrieves alarm definitions for alertmanager notification", func() { - fp := "9a9e2d82a78cf2b9" - clusterID := uuid.New() - alertname := "CollectorNodeDown" - severity := "info" - am := &api.AlertmanagerNotification{ - Alerts: []api.Alert{ - { - Annotations: nil, - Fingerprint: &fp, - Labels: &map[string]string{ - "alertname": alertname, - "severity": severity, - "managed_cluster": clusterID.String(), - }, - }, - }, - } - - objectTypeID := uuid.New() - clusterMap := map[uuid.UUID]uuid.UUID{clusterID: objectTypeID} - - mock.ExpectQuery(fmt.Sprintf("SELECT (.+) FROM %s WHERE", models.AlarmDefinition{}.TableName())). - WithArgs(alertname, objectTypeID, severity). - WillReturnRows(pgxmock.NewRows([]string{ - "alarm_name", "alarm_definition_id", "probable_cause_id", - "object_type_id", "severity", - }).AddRow( - alertname, uuid.New(), uuid.New(), - objectTypeID, severity, - )) - - results, err := repo.GetAlarmDefinitions(ctx, am, clusterMap) - Expect(err).NotTo(HaveOccurred()) - Expect(results).To(HaveLen(1)) - Expect(results[0].AlarmName).To(Equal(alertname)) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - When("an invalid alertmanager notification is provided", func() { - It("it returns an empty list of definition", func() { - fp := "9a9e2d82a78cf2b9" - clusterID := uuid.New() - alertname := "CollectorNodeDown" - severity := "info" - am := &api.AlertmanagerNotification{ - Alerts: []api.Alert{ - { - Annotations: nil, - Fingerprint: &fp, - Labels: &map[string]string{ - "alertname": alertname, - "severity": severity, - "managed_cluster": clusterID.String(), - }, - }, - }, - } - - // force objectID not found - clusterMap := map[uuid.UUID]uuid.UUID{} - - results, err := repo.GetAlarmDefinitions(ctx, am, clusterMap) - Expect(err).NotTo(HaveOccurred()) - Expect(results).To(HaveLen(0)) - Expect(mock.ExpectationsWereMet()).NotTo(HaveOccurred()) - }) - }) - }) Describe("GetServiceConfigurations", func() { When("records exist", func() { It("returns all service configurations", func() { diff --git a/internal/service/alarms/internal/db/repo/generated/mock_repo.generated.go b/internal/service/alarms/internal/db/repo/generated/mock_repo.generated.go index 9cc0095f..d9ebca58 100644 --- a/internal/service/alarms/internal/db/repo/generated/mock_repo.generated.go +++ b/internal/service/alarms/internal/db/repo/generated/mock_repo.generated.go @@ -71,35 +71,6 @@ func (mr *MockAlarmRepositoryInterfaceMockRecorder) CreateServiceConfiguration(c return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateServiceConfiguration", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).CreateServiceConfiguration), ctx, defaultRetentionPeriod) } -// DeleteAlarmDefinitionsNotIn mocks base method. -func (m *MockAlarmRepositoryInterface) DeleteAlarmDefinitionsNotIn(ctx context.Context, ids []any, objectTypeID uuid.UUID) (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAlarmDefinitionsNotIn", ctx, ids, objectTypeID) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DeleteAlarmDefinitionsNotIn indicates an expected call of DeleteAlarmDefinitionsNotIn. -func (mr *MockAlarmRepositoryInterfaceMockRecorder) DeleteAlarmDefinitionsNotIn(ctx, ids, objectTypeID any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAlarmDefinitionsNotIn", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).DeleteAlarmDefinitionsNotIn), ctx, ids, objectTypeID) -} - -// DeleteAlarmDictionariesNotIn mocks base method. -func (m *MockAlarmRepositoryInterface) DeleteAlarmDictionariesNotIn(ctx context.Context, ids []any) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteAlarmDictionariesNotIn", ctx, ids) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteAlarmDictionariesNotIn indicates an expected call of DeleteAlarmDictionariesNotIn. -func (mr *MockAlarmRepositoryInterfaceMockRecorder) DeleteAlarmDictionariesNotIn(ctx, ids any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAlarmDictionariesNotIn", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).DeleteAlarmDictionariesNotIn), ctx, ids) -} - // DeleteAlarmSubscription mocks base method. func (m *MockAlarmRepositoryInterface) DeleteAlarmSubscription(ctx context.Context, id uuid.UUID) (int64, error) { m.ctrl.T.Helper() @@ -115,36 +86,6 @@ func (mr *MockAlarmRepositoryInterfaceMockRecorder) DeleteAlarmSubscription(ctx, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAlarmSubscription", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).DeleteAlarmSubscription), ctx, id) } -// GetAlarmDefinition mocks base method. -func (m *MockAlarmRepositoryInterface) GetAlarmDefinition(ctx context.Context, id uuid.UUID) (*models.AlarmDefinition, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAlarmDefinition", ctx, id) - ret0, _ := ret[0].(*models.AlarmDefinition) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAlarmDefinition indicates an expected call of GetAlarmDefinition. -func (mr *MockAlarmRepositoryInterfaceMockRecorder) GetAlarmDefinition(ctx, id any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlarmDefinition", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).GetAlarmDefinition), ctx, id) -} - -// GetAlarmDefinitions mocks base method. -func (m *MockAlarmRepositoryInterface) GetAlarmDefinitions(ctx context.Context, am *generated.AlertmanagerNotification, clusterMap map[uuid.UUID]uuid.UUID) ([]models.AlarmDefinition, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetAlarmDefinitions", ctx, am, clusterMap) - ret0, _ := ret[0].([]models.AlarmDefinition) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetAlarmDefinitions indicates an expected call of GetAlarmDefinitions. -func (mr *MockAlarmRepositoryInterfaceMockRecorder) GetAlarmDefinitions(ctx, am, clusterMap any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlarmDefinitions", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).GetAlarmDefinitions), ctx, am, clusterMap) -} - // GetAlarmEventRecord mocks base method. func (m *MockAlarmRepositoryInterface) GetAlarmEventRecord(ctx context.Context, id uuid.UUID) (*models.AlarmEventRecord, error) { m.ctrl.T.Helper() @@ -308,36 +249,6 @@ func (mr *MockAlarmRepositoryInterfaceMockRecorder) UpdateSubscriptionEventCurso return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubscriptionEventCursor", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).UpdateSubscriptionEventCursor), ctx, subscription) } -// UpsertAlarmDefinitions mocks base method. -func (m *MockAlarmRepositoryInterface) UpsertAlarmDefinitions(ctx context.Context, records []models.AlarmDefinition) ([]models.AlarmDefinition, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertAlarmDefinitions", ctx, records) - ret0, _ := ret[0].([]models.AlarmDefinition) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpsertAlarmDefinitions indicates an expected call of UpsertAlarmDefinitions. -func (mr *MockAlarmRepositoryInterfaceMockRecorder) UpsertAlarmDefinitions(ctx, records any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAlarmDefinitions", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).UpsertAlarmDefinitions), ctx, records) -} - -// UpsertAlarmDictionary mocks base method. -func (m *MockAlarmRepositoryInterface) UpsertAlarmDictionary(ctx context.Context, record models.AlarmDictionary) ([]models.AlarmDictionary, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "UpsertAlarmDictionary", ctx, record) - ret0, _ := ret[0].([]models.AlarmDictionary) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// UpsertAlarmDictionary indicates an expected call of UpsertAlarmDictionary. -func (mr *MockAlarmRepositoryInterfaceMockRecorder) UpsertAlarmDictionary(ctx, record any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAlarmDictionary", reflect.TypeOf((*MockAlarmRepositoryInterface)(nil).UpsertAlarmDictionary), ctx, record) -} - // UpsertAlarmEventRecord mocks base method. func (m *MockAlarmRepositoryInterface) UpsertAlarmEventRecord(ctx context.Context, records []models.AlarmEventRecord) error { m.ctrl.T.Helper() diff --git a/internal/service/alarms/internal/dictionary_collector/cluster.go b/internal/service/alarms/internal/dictionary_collector/cluster.go deleted file mode 100644 index e9f24e45..00000000 --- a/internal/service/alarms/internal/dictionary_collector/cluster.go +++ /dev/null @@ -1,452 +0,0 @@ -package dictionary_collector - -import ( - "context" - "encoding/json" - "fmt" - "log/slog" - "sync" - "time" - - "github.com/google/uuid" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - "golang.org/x/sync/errgroup" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" - clusterv1 "open-cluster-management.io/api/cluster/v1" - crclient "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/openshift-kni/oran-o2ims/internal/controllers/utils" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/models" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/repo" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver" - "github.com/openshift-kni/oran-o2ims/internal/service/common/clients" - "github.com/openshift-kni/oran-o2ims/internal/service/common/clients/k8s" -) - -type NodeClusterTypeDictionaryService struct { - AlarmsRepository *repo.AlarmsRepository - HubClient crclient.Client - - RulesMap map[uuid.UUID][]monitoringv1.Rule -} - -// loadNodeClusterTypeDictionaries loads the alarm dictionaries for the given node cluster types -func (r *NodeClusterTypeDictionaryService) loadNodeClusterTypeDictionaries(ctx context.Context, nodeClusterTypes []clusterserver.NodeClusterType) { - slog.Info("loading alarm dictionaries for node cluster types") - - if len(nodeClusterTypes) == 0 { - slog.Info("no node cluster types to process") - return - } - - type result struct { - NodeClusterTypeID uuid.UUID - rules []monitoringv1.Rule - err error - } - - wg := sync.WaitGroup{} - resultChannel := make(chan result) - for _, nct := range nodeClusterTypes { - wg.Add(1) - go func(nodeClusterType clusterserver.NodeClusterType) { - var err error - var rules []monitoringv1.Rule - - defer func() { - resultChannel <- result{ - NodeClusterTypeID: nodeClusterType.NodeClusterTypeId, - rules: rules, - err: err, - } - wg.Done() - }() - - extensions, err := getVendorExtensions(nodeClusterType) - if err != nil { - // Should never happen - err = fmt.Errorf("error getting vendor extensions: %w", err) - return - } - - switch extensions.model { - case utils.ClusterModelManagedCluster: - rules, err = r.processManagedCluster(ctx, extensions.version) - case utils.ClusterModelHubCluster: - rules, err = r.processHub(ctx) - default: - err = fmt.Errorf("unsupported node cluster type: %s", extensions.model) - } - }(nct) - } - - go func() { - wg.Wait() - close(resultChannel) - }() - - for res := range resultChannel { - if res.err != nil { - slog.Error("error fetching rules for node cluster type", "NodeClusterType ID", res.NodeClusterTypeID, "error", res.err) - continue - } - - r.RulesMap[res.NodeClusterTypeID] = res.rules - slog.Info("loaded rules for node cluster type", "NodeClusterType ID", res.NodeClusterTypeID, "rules count", len(res.rules)) - } - - err := r.syncDictionaries(ctx, nodeClusterTypes) - if err != nil { - slog.Error("failed to sync dictionary and definitions", "error", err) - } -} - -// processHub processes the hub cluster -func (r *NodeClusterTypeDictionaryService) processHub(ctx context.Context) ([]monitoringv1.Rule, error) { - rules, err := r.getRules(ctx, r.HubClient) - if err != nil { - return nil, err - } - - slog.Debug("fetched rules for Hub cluster", "rules count", len(rules)) - return rules, nil -} - -// Needed for testing -var getClientForCluster = k8s.NewClientForCluster - -// processManagedCluster processes a managed cluster -func (r *NodeClusterTypeDictionaryService) processManagedCluster(ctx context.Context, version string) ([]monitoringv1.Rule, error) { - cluster, err := r.getManagedCluster(ctx, version) - if err != nil { - return nil, err - } - - cl, err := getClientForCluster(ctx, r.HubClient, cluster.Name) - if err != nil { - return nil, err - } - - rules, err := r.getRules(ctx, cl) - if err != nil { - return nil, err - } - - slog.Debug("fetched rules for managed cluster", "cluster", cluster.Name, "version", version, "rules count", len(rules)) - return rules, nil -} - -// getManagedCluster finds a single managed cluster with the given version -func (r *NodeClusterTypeDictionaryService) getManagedCluster(ctx context.Context, version string) (*clusterv1.ManagedCluster, error) { - // Match managed cluster with the given version and not local cluster - selector := labels.NewSelector() - versionSelector, _ := labels.NewRequirement(utils.OpenshiftVersionLabelName, selection.Equals, []string{version}) - localClusterRequirement, _ := labels.NewRequirement(utils.LocalClusterLabelName, selection.NotEquals, []string{"true"}) - - ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) - defer cancel() - - var managedClusters clusterv1.ManagedClusterList - err := r.HubClient.List(ctxWithTimeout, &managedClusters, &crclient.ListOptions{ - LabelSelector: selector.Add(*versionSelector).Add(*localClusterRequirement), - Limit: 1, - }) - if err != nil { - return nil, fmt.Errorf("error listing managed clusters: %w", err) - } - - if len(managedClusters.Items) == 0 { - return nil, fmt.Errorf("no managed cluster found with version %s", version) - } - - return &managedClusters.Items[0], nil -} - -// getRules gets rules defined within a PrometheusRule -func (r *NodeClusterTypeDictionaryService) getRules(ctx context.Context, cl crclient.Client) ([]monitoringv1.Rule, error) { - ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) - defer cancel() - - var promRules monitoringv1.PrometheusRuleList - err := cl.List(ctxWithTimeout, &promRules) - if err != nil { - return nil, fmt.Errorf("error listing prometheus rules: %w", err) - } - - // Extract rules from PrometheusRule list - var rules []monitoringv1.Rule - for _, promRule := range promRules.Items { - for _, group := range promRule.Spec.Groups { - for _, rule := range group.Rules { - // Only alerting rules are of interest (not recording rules) - if rule.Alert != "" { - rules = append(rules, rule) - } - } - } - } - - return rules, nil -} - -// syncDictionaries synchronizes the alarm dictionaries in the database -func (r *NodeClusterTypeDictionaryService) syncDictionaries(ctx context.Context, nodeClusterTypes []clusterserver.NodeClusterType) error { - slog.Info("Synchronizing alarm dictionaries in the database") - ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) // let's try to finish init with a set time. - defer cancel() - - // First clean up outdated dictionaries - r.deleteOutdatedDictionaries(ctx, nodeClusterTypes) - - // Then process all NodeClusterTypes concurrently - if err := r.processNodeClusterTypes(ctx, nodeClusterTypes); err != nil { - return fmt.Errorf("failed to process some NodeClusterTypes: %w", err) - } - - slog.Info("Alarm dictionaries and corresponding AlarmDefinitions synchronized") - return nil -} - -// deleteOutdatedDictionaries remove any dictionary that may not be available anymore -func (r *NodeClusterTypeDictionaryService) deleteOutdatedDictionaries(ctx context.Context, nodeClusterTypes []clusterserver.NodeClusterType) { - // Delete Dictionaries that do not have a corresponding NodeClusterType - ids := make([]any, 0, len(nodeClusterTypes)) - for _, nodeClusterType := range nodeClusterTypes { - ids = append(ids, nodeClusterType.NodeClusterTypeId) - } - - if err := r.AlarmsRepository.DeleteAlarmDictionariesNotIn(ctx, ids); err != nil { - slog.Warn("failed to delete alarm dictionaries", "error", err) - } - - slog.Info("Outdated alarm dictionaries deleted successfully") -} - -// processNodeClusterTypes process each nodeClusterType in parallel and return early with error if anything fails -func (r *NodeClusterTypeDictionaryService) processNodeClusterTypes(ctx context.Context, nodeClusterTypes []clusterserver.NodeClusterType) error { - // Validate all rules exist before starting any processing - r.validateRulesExist(nodeClusterTypes) - - g, ctx := errgroup.WithContext(ctx) - g.SetLimit(5) // Limit concurrent operations to avoid overwhelming the database - - for _, rt := range nodeClusterTypes { - nodeClusterType := rt // Create new variable to avoid data race - g.Go(func() error { - return r.processNodeClusterType(ctx, nodeClusterType) - }) - } - slog.Info("Waiting for all nodeClusterTypes to processed") - return g.Wait() //nolint:wrapcheck -} - -// validateRulesExist check if we have rules to work with that will be converted to alarm defs and warn otherwise -func (r *NodeClusterTypeDictionaryService) validateRulesExist(nodeClusterTypes []clusterserver.NodeClusterType) { - var missingRules []uuid.UUID - for _, nct := range nodeClusterTypes { - if _, ok := r.RulesMap[nct.NodeClusterTypeId]; !ok { - missingRules = append(missingRules, nct.NodeClusterTypeId) - } - } - - if len(missingRules) > 0 { - slog.Warn("Rules missing for nodeClustertype", "missingRules", missingRules) - } -} - -// processNodeClusterType process dict and def for nodeClusterType -func (r *NodeClusterTypeDictionaryService) processNodeClusterType(ctx context.Context, nodeClusterType clusterserver.NodeClusterType) error { - // Process dictionary_definition - dictID, err := r.upsertAlarmDictionary(ctx, nodeClusterType) - if err != nil { - return fmt.Errorf("failed to upsert alarm dictionary_definition for NodeClusterType %s: %w", - nodeClusterType.NodeClusterTypeId, err) - } - - // Process alarm definitions - if err := r.processAlarmDefinitions(ctx, nodeClusterType, dictID); err != nil { - return fmt.Errorf("failed to process alarm definitions for NodeClusterType %s: %w", - nodeClusterType.NodeClusterTypeId, err) - } - - slog.Info("Successfully processed nodeClusterType", "nodeClusterType", nodeClusterType.NodeClusterTypeId) - return nil -} - -// upsertAlarmDictionary process dict for nodeClusterType -func (r *NodeClusterTypeDictionaryService) upsertAlarmDictionary(ctx context.Context, nodeClusterType clusterserver.NodeClusterType) (models.AlarmDictionary, error) { - extensions, err := getVendorExtensions(nodeClusterType) - if err != nil { - return models.AlarmDictionary{}, fmt.Errorf("failed to get extensions for nodeClusterType %s: %w", nodeClusterType.NodeClusterTypeId, err) - } - // Alarm Dictionary record - alarmDict := models.AlarmDictionary{ - AlarmDictionaryVersion: extensions.version, - EntityType: fmt.Sprintf("%s-%s", extensions.model, extensions.version), - Vendor: extensions.version, - ObjectTypeID: nodeClusterType.NodeClusterTypeId, - } - - // Upsert Alarm Dictionary - alarmDictRecords, err := r.AlarmsRepository.UpsertAlarmDictionary(ctx, alarmDict) - if err != nil { - return models.AlarmDictionary{}, fmt.Errorf("failed to upsert alarm dictionary_definition: %w", err) - } - - if len(alarmDictRecords) != 1 { - return models.AlarmDictionary{}, fmt.Errorf("unexpected number of Alarm Dictionary records, expected 1, got %d", len(alarmDictRecords)) - } - - slog.Debug("Alarm dictionary upserted", "nodeClusterType", nodeClusterType.NodeClusterTypeId, "alarmDictionaryID", alarmDictRecords[0].AlarmDictionaryID) - return alarmDictRecords[0], nil -} - -// upsertAlarmDictionary process def for nodeClusterType -func (r *NodeClusterTypeDictionaryService) processAlarmDefinitions(ctx context.Context, nodeClusterType clusterserver.NodeClusterType, ad models.AlarmDictionary) error { - // Get filtered rules - filteredRules := r.getFilteredRules(nodeClusterType.NodeClusterTypeId) - - // Create alarm definitions - records := r.createAlarmDefinitions(filteredRules, ad, nodeClusterType) - - // Upsert and cleanup - if err := r.upsertAndCleanupDefinitions(ctx, records, nodeClusterType.NodeClusterTypeId); err != nil { - return fmt.Errorf("failed to upsert and cleanup definitions: %w", err) - } - - slog.Info("Successfully processed all AlarmDefinitions", "nodeClusterType", nodeClusterType.NodeClusterTypeId, "alarmDict", ad.AlarmDictionaryID) - return nil -} - -// getFilteredRules check to see if rule can potentially be skipped -func (r *NodeClusterTypeDictionaryService) getFilteredRules(nodeClusterTypeID uuid.UUID) []monitoringv1.Rule { - // Upsert will complain if there are rules with the same Alert and Severity - // We need to filter them out. First occurrence wins. - type uniqueAlarm struct { - Alert string - Severity string - } - - var filteredRules []monitoringv1.Rule - exist := make(map[uniqueAlarm]bool) - - for _, rule := range r.RulesMap[nodeClusterTypeID] { - severity, ok := rule.Labels["severity"] - if !ok { - slog.Warn("rule missing severity label", "alert", rule.Alert, "nodeClusterTypeID", nodeClusterTypeID) - } - - key := uniqueAlarm{ - Alert: rule.Alert, - Severity: severity, - } - - if !exist[key] { - exist[key] = true - filteredRules = append(filteredRules, rule) - } else { - slog.Warn("Duplicate rules found", "nodeClusterTypeID", nodeClusterTypeID, "rule", rule) - } - } - return filteredRules -} - -// createAlarmDefinitions create new alarm def for each rules -func (r *NodeClusterTypeDictionaryService) createAlarmDefinitions(rules []monitoringv1.Rule, ad models.AlarmDictionary, nodeClusterType clusterserver.NodeClusterType) []models.AlarmDefinition { - var records []models.AlarmDefinition - - for _, rule := range rules { - additionalFields := map[string]string{"Expr": rule.Expr.String()} - if rule.For != nil { - additionalFields["For"] = string(*rule.For) - } - if rule.KeepFiringFor != nil { - additionalFields["KeepFiringFor"] = string(*rule.KeepFiringFor) - } - - ntc, _ := json.Marshal(nodeClusterType) - additionalFields["NodeClusterTypeData"] = string(ntc) - - //TODO: Add info from prometheus rules containing the rule such as the namespace - - summary := rule.Annotations["summary"] - description := rule.Annotations["description"] - runbookURL := rule.Annotations["runbook_url"] - - records = append(records, models.AlarmDefinition{ - AlarmName: rule.Alert, - AlarmLastChange: ad.AlarmDictionaryVersion, - AlarmDescription: fmt.Sprintf("Summary: %s\nDescription: %s", summary, description), - ProposedRepairActions: runbookURL, - AlarmAdditionalFields: additionalFields, - AlarmDictionaryID: ad.AlarmDictionaryID, - Severity: rule.Labels["severity"], - }) - } - - slog.Info("AlarmDefinitions from promrules prepared", "count", len(records)) - return records -} - -// upsertAndCleanupDefinitions insert or update and finally remove defs if possible -func (r *NodeClusterTypeDictionaryService) upsertAndCleanupDefinitions(ctx context.Context, records []models.AlarmDefinition, nodeClusterTypeID uuid.UUID) error { - // Upsert Alarm Definitions - alarmDefinitionRecords, err := r.AlarmsRepository.UpsertAlarmDefinitions(ctx, records) - if err != nil { - return fmt.Errorf("failed to upsert alarm definitions: %w", err) - } - - slog.Info("Alarm definitions upserted", "nodeClusterTypeID", nodeClusterTypeID, "count", len(alarmDefinitionRecords)) - - // Delete Alarm Definitions that were not upserted - alarmDefinitionIDs := make([]any, 0, len(alarmDefinitionRecords)) - for _, record := range alarmDefinitionRecords { - alarmDefinitionIDs = append(alarmDefinitionIDs, record.AlarmDefinitionID) - } - - count, err := r.AlarmsRepository.DeleteAlarmDefinitionsNotIn(ctx, alarmDefinitionIDs, nodeClusterTypeID) - if err != nil { - return fmt.Errorf("failed to delete alarm definitions: %w", err) - } - if count > 0 { - slog.Info("Alarm definitions synced", "nodeClusterTypeID", nodeClusterTypeID, "delete count", count) - } - - return nil -} - -type vendorExtensions struct { - model string - version string - vendor string -} - -// getVendorExtensions gets the vendor extensions from the node cluster type. -// Should never return an error. -func getVendorExtensions(nodeClusterType clusterserver.NodeClusterType) (*vendorExtensions, error) { - if nodeClusterType.Extensions == nil { - return nil, fmt.Errorf("no extensions found for node cluster type %d", nodeClusterType.NodeClusterTypeId) - } - - model, ok := (*nodeClusterType.Extensions)[utils.ClusterModelExtension].(string) - if !ok { - return nil, fmt.Errorf("no model extension found for node cluster type %s", nodeClusterType.NodeClusterTypeId) - } - - version, ok := (*nodeClusterType.Extensions)[utils.ClusterVersionExtension].(string) - if !ok { - return nil, fmt.Errorf("no version extension found for node cluster type %s", nodeClusterType.NodeClusterTypeId) - } - - vendor, ok := (*nodeClusterType.Extensions)[utils.ClusterVendorExtension].(string) - if !ok { - return nil, fmt.Errorf("no vendor extension found for node cluster type %s", nodeClusterType.NodeClusterTypeId) - } - - return &vendorExtensions{ - model: model, - version: version, - vendor: vendor, - }, nil -} diff --git a/internal/service/alarms/internal/dictionary_collector/collector.go b/internal/service/alarms/internal/dictionary_collector/collector.go deleted file mode 100644 index 1176b158..00000000 --- a/internal/service/alarms/internal/dictionary_collector/collector.go +++ /dev/null @@ -1,83 +0,0 @@ -package dictionary_collector - -import ( - "context" - "fmt" - "log/slog" - "time" - - "github.com/go-logr/logr" - "github.com/google/uuid" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - crclient "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/repo" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver" - "github.com/openshift-kni/oran-o2ims/internal/service/common/clients/k8s" -) - -const pollInterval = 10 * time.Minute - -// Collector is the struct that holds the alarms repository and the infrastructure clients -type Collector struct { - AlarmsRepository *repo.AlarmsRepository - Infrastructure *infrastructure.Infrastructure - - hubClient crclient.Client -} - -func New(ar *repo.AlarmsRepository, infra *infrastructure.Infrastructure) (*Collector, error) { - ad := &Collector{ - AlarmsRepository: ar, - Infrastructure: infra, - } - - // To avoid log from eventuallyFulfillRoot controller-runtime - log.SetLogger(logr.Discard()) - - // Create a client for the hub cluster - hubClient, err := k8s.NewClientForHub() - if err != nil { - return nil, fmt.Errorf("failed to create client for hub cluster: %w", err) - } - - ad.hubClient = hubClient - - return ad, nil -} - -// Run starts the alarm dictionary collector -func (r *Collector) Run(ctx context.Context) { - // Currently only the cluster server is supported - for i := range r.Infrastructure.Clients { - if r.Infrastructure.Clients[i].Name() == clusterserver.Name { - r.executeNodeClusterTypeDictionaryService(ctx, r.Infrastructure.Clients[i].(*clusterserver.ClusterServer)) - } - } -} - -// executeNodeClusterTypeDictionaryService starts the NodeClusterTypeDictionaryService -func (r *Collector) executeNodeClusterTypeDictionaryService(ctx context.Context, clusterServerClient *clusterserver.ClusterServer) { - c := &NodeClusterTypeDictionaryService{ - AlarmsRepository: r.AlarmsRepository, - HubClient: r.hubClient, - RulesMap: make(map[uuid.UUID][]monitoringv1.Rule), - } - - // First execution - c.loadNodeClusterTypeDictionaries(ctx, clusterServerClient.GetNodeClusterTypes()) - - go func() { - for { - select { - case <-ctx.Done(): - slog.Info("context cancelled, stopping node cluster type dictionary service") - return - case <-time.After(pollInterval): - c.loadNodeClusterTypeDictionaries(ctx, clusterServerClient.GetNodeClusterTypes()) - } - } - }() -} diff --git a/internal/service/alarms/internal/dictionary_collector/dictionary_collector_test.go b/internal/service/alarms/internal/dictionary_collector/dictionary_collector_test.go deleted file mode 100644 index a3cc7a71..00000000 --- a/internal/service/alarms/internal/dictionary_collector/dictionary_collector_test.go +++ /dev/null @@ -1,223 +0,0 @@ -package dictionary_collector - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/openshift-kni/oran-o2ims/internal/controllers/utils" - monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/util/intstr" - clusterv1 "open-cluster-management.io/api/cluster/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/fake" -) - -const ( - version4167 = "4.16.7" - version4152 = "4.15.2" -) - -var _ = Describe("Alarm dictionary collector for node cluster type", func() { - Describe("getManagedCluster", func() { - var ( - r *NodeClusterTypeDictionaryService - ctx context.Context - scheme *runtime.Scheme - ) - - BeforeEach(func() { - scheme = runtime.NewScheme() - _ = clusterv1.AddToScheme(scheme) - - withWatch := fake.NewClientBuilder().WithScheme(scheme).Build() - r = &NodeClusterTypeDictionaryService{ - HubClient: withWatch, - } - - ctx = context.Background() - - managedClusters := []*clusterv1.ManagedCluster{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "cluster-1", - Namespace: "default", - Labels: map[string]string{ - utils.OpenshiftVersionLabelName: version4167, - utils.LocalClusterLabelName: "true", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "cluster-2", - Namespace: "default", - Labels: map[string]string{ - utils.OpenshiftVersionLabelName: version4167, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "cluster-3", - Namespace: "default", - Labels: map[string]string{ - utils.OpenshiftVersionLabelName: version4167, - utils.LocalClusterLabelName: "true", - }, - }, - }, - } - - for _, cluster := range managedClusters { - err := r.HubClient.Create(ctx, cluster) - Expect(err).NotTo(HaveOccurred()) - } - }) - - It("returns a cluster with the correct version", func() { - cluster, err := r.getManagedCluster(ctx, version4167) - Expect(err).NotTo(HaveOccurred()) - - Expect(cluster.Labels[utils.OpenshiftVersionLabelName]).To(Equal(version4167)) - Expect(cluster.Labels).ToNot(HaveKey(utils.LocalClusterLabelName)) - }) - It("returns an error when no cluster is found", func() { - _, err := r.getManagedCluster(ctx, version4152) - Expect(err).To(HaveOccurred()) - }) - }) - Describe("processManagedCluster", func() { - var ( - r *NodeClusterTypeDictionaryService - ctx context.Context - scheme *runtime.Scheme - - temp func(ctx context.Context, hubClient client.Client, clusterName string) (client.Client, error) - ) - - BeforeEach(func() { - scheme = runtime.NewScheme() - _ = clusterv1.AddToScheme(scheme) - - withWatch := fake.NewClientBuilder().WithScheme(scheme).Build() - r = &NodeClusterTypeDictionaryService{ - HubClient: withWatch, - } - - ctx = context.Background() - - managedCluster := &clusterv1.ManagedCluster{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cluster-1", - Namespace: "default", - Labels: map[string]string{ - utils.OpenshiftVersionLabelName: version4167, - }, - }, - } - - err := r.HubClient.Create(ctx, managedCluster) - Expect(err).NotTo(HaveOccurred()) - - temp = getClientForCluster - getClientForCluster = func(ctx context.Context, hubClient client.Client, clusterName string) (client.Client, error) { - scheme := runtime.NewScheme() - _ = monitoringv1.AddToScheme(scheme) - withWatch := fake.NewClientBuilder().WithScheme(scheme).Build() - - rules := []*monitoringv1.PrometheusRule{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "acm-metrics-collector-alerting-rules", - Namespace: "monitoring", - }, - Spec: monitoringv1.PrometheusRuleSpec{ - Groups: []monitoringv1.RuleGroup{ - { - Name: "metrics-collector-rules", - Rules: []monitoringv1.Rule{ - { - Alert: "ACMMetricsCollectorFederationError", - Annotations: map[string]string{ - "summary": "Error federating from in-cluster Prometheus.", - "description": "There are errors when federating from platform Prometheus", - }, - Expr: intstr.IntOrString{ - Type: intstr.String, - IntVal: 0, - StrVal: "(sum by (status_code, type) (rate(acm_metrics_collector_federate_requests_total{status_code!~\"2.*\"}[10m]))) > 10", - }, - For: func() *monitoringv1.Duration { - d := monitoringv1.Duration("10m") - return &d - }(), - Labels: map[string]string{ - "severity": "critical", - }, - }, - }, - }, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "machineapprover-rules", - Namespace: "monitoring", - }, - Spec: monitoringv1.PrometheusRuleSpec{ - Groups: []monitoringv1.RuleGroup{ - { - Name: "memory-alerts", - Rules: []monitoringv1.Rule{ - { - Alert: "HighMemoryUsage", - Annotations: map[string]string{ - "summary": "High Memory Usage detected", - "description": "Memory usage is above 85% for more than 5 minutes on instance {{ $labels.instance }}", - }, - Expr: intstr.IntOrString{ - Type: intstr.String, - IntVal: 0, - StrVal: "mapi_current_pending_csr > mapi_max_pending_csr", - }, - For: func() *monitoringv1.Duration { - d := monitoringv1.Duration("5m") - return &d - }(), - Labels: map[string]string{ - "severity": "critical", - }, - }, - }, - }, - }, - }, - }, - } - - for _, rule := range rules { - err := withWatch.Create(ctx, rule) - Expect(err).NotTo(HaveOccurred()) - } - - return withWatch, nil - } - }) - - AfterEach(func() { - getClientForCluster = temp - }) - - It("returns prometheus rules associated with a cluster", func() { - rules, err := r.processManagedCluster(ctx, version4167) - Expect(err).NotTo(HaveOccurred()) - - Expect(rules).To(HaveLen(2)) - }) - }) -}) diff --git a/internal/service/alarms/internal/dictionary_collector/suite_test.go b/internal/service/alarms/internal/dictionary_collector/suite_test.go deleted file mode 100644 index 007e8fdd..00000000 --- a/internal/service/alarms/internal/dictionary_collector/suite_test.go +++ /dev/null @@ -1,13 +0,0 @@ -package dictionary_collector_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestDictionary(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Dictionary Definition Suite") -} diff --git a/internal/service/alarms/internal/infrastructure/clusterserver.go b/internal/service/alarms/internal/infrastructure/clusterserver.go new file mode 100644 index 00000000..a3d98535 --- /dev/null +++ b/internal/service/alarms/internal/infrastructure/clusterserver.go @@ -0,0 +1,439 @@ +package infrastructure + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "sync" + "time" + + "github.com/google/uuid" + "k8s.io/client-go/transport" + + "github.com/openshift-kni/oran-o2ims/internal/controllers/utils" + "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver/generated" + "github.com/openshift-kni/oran-o2ims/internal/service/common/clients" +) + +const ( + Name = "Cluster" + + clusterServerURLEnvName = "CLUSTER_SERVER_URL" + tokenPathEnvName = "TOKEN_PATH" +) + +type NodeCluster = generated.NodeCluster +type NodeClusterType = generated.NodeClusterType + +type ClusterServer struct { + client *generated.ClientWithResponses + nodeClusterIDToNodeClusterTypeID map[uuid.UUID]uuid.UUID + nodeClusterTypeIDToAlarmDictionaryID map[uuid.UUID]uuid.UUID + alarmDictionaryIDToAlarmDefinitions map[uuid.UUID]AlarmDefinition + + sync.Mutex +} + +// Name returns the name of the client +func (r *ClusterServer) Name() string { + return Name +} + +// Setup setups a new client for the cluster server +func (r *ClusterServer) Setup() error { + slog.Info("Creating ClusterServer client") + + url := utils.GetServiceURL(utils.InventoryClusterServerName) + + // Use for local development + clusterServerURL := os.Getenv(clusterServerURLEnvName) + if clusterServerURL != "" { + url = clusterServerURL + } + + // Set up transport + tr, err := utils.GetDefaultBackendTransport() + if err != nil { + return fmt.Errorf("failed to create http transport: %w", err) + } + + hc := http.Client{Transport: tr} + + tokenPath := utils.DefaultBackendTokenFile + + // Use for local development + path := os.Getenv(tokenPathEnvName) + if path != "" { + tokenPath = path + } + + // Create a request editor that uses a cached token source capable of re-reading from file to pickup changes + // as our token is renewed. + editor := clients.AuthorizationEditor{ + Source: transport.NewCachedFileTokenSource(tokenPath), + } + c, err := generated.NewClientWithResponses(url, generated.WithHTTPClient(&hc), generated.WithRequestEditorFn(editor.Editor)) + if err != nil { + return fmt.Errorf("failed to create client: %w", err) + } + + r.client = c + r.nodeClusterIDToNodeClusterTypeID = make(map[uuid.UUID]uuid.UUID) + r.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + r.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + + return nil +} + +// FetchAll fetches all necessary data from the cluster server +func (r *ClusterServer) FetchAll(ctx context.Context) error { + slog.Info("Getting all objects from the cluster server") + + // List node clusters + nodeClusters, err := r.getNodeClusters(ctx) + if err != nil { + return fmt.Errorf("failed to get node clusters: %w", err) + } + + // List node cluster types + nodeClusterTypes, err := r.getNodeClusterTypes(ctx) + if err != nil { + return fmt.Errorf("failed to get node cluster types: %w", err) + } + + // List alarm dictionaries + alarmDictionaries, err := r.getAlarmDictionaries(ctx) + if err != nil { + return fmt.Errorf("failed to get alarm dictionaries: %w", err) + } + + r.Lock() + defer r.Unlock() + + r.nodeClusterIDToNodeClusterTypeID = r.buildNodeClusterIDToNodeClusterTypeID(nodeClusters) + r.nodeClusterTypeIDToAlarmDictionaryID = r.buildNodeClusterTypeIDToAlarmDictionaryID(nodeClusterTypes) + r.alarmDictionaryIDToAlarmDefinitions = r.buildAlarmDictionaryIDToAlarmDefinitions(alarmDictionaries) + + return nil +} + +// GetObjectTypeID gets the node cluster type ID for a given node cluster ID +// It uses the cache if available, otherwise it fetches the data from the server +func (r *ClusterServer) GetObjectTypeID(nodeClusterID uuid.UUID) (uuid.UUID, error) { + r.Lock() + defer r.Unlock() + + nodeClusterTypeID, ok := r.nodeClusterIDToNodeClusterTypeID[nodeClusterID] + if !ok { + slog.Info("Node cluster ID not found in cache", "nodeClusterID", nodeClusterID) + + // Try to fetch it from the server + nodeCluster, err := r.getNodeCluster(context.Background(), nodeClusterID) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to fetch node cluster type ID: %w", err) + } + + nodeClusterTypeID = nodeCluster.NodeClusterTypeId + r.nodeClusterIDToNodeClusterTypeID[nodeClusterID] = nodeClusterTypeID + slog.Info("Mapping node cluster ID to node cluster type ID", "nodeClusterID", nodeClusterID, "nodeClusterTypeID", nodeClusterTypeID) + } + + return nodeClusterTypeID, nil +} + +// GetAlarmDefinitionID gets the alarm definition ID for a given node cluster type ID, name and severity +// It uses the cache if available, otherwise it fetches the data from the server +func (r *ClusterServer) GetAlarmDefinitionID(nodeClusterTypeID uuid.UUID, name, severity string) (uuid.UUID, error) { + r.Lock() + defer r.Unlock() + + alarmDictionaryID, ok := r.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] + if !ok { + slog.Info("Node Cluster Type ID not found in cache", "nodeClusterTypeID", nodeClusterTypeID) + + // Try to fetch it from the server + nodeClusterType, err := r.getNodeClusterType(context.Background(), nodeClusterTypeID) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to fetch alarm dictionary ID: %w", err) + } + + alarmDictionaryID, err = getAlarmDictionaryIDFromNodeClusterType(nodeClusterType) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to get alarm dictionary ID from node cluster type object: %w", err) + } + + r.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] = alarmDictionaryID + slog.Info("Mapping node cluster type ID to alarm dictionary ID", "nodeClusterTypeID", nodeClusterTypeID, "alarmDictionaryID", alarmDictionaryID) + } + + definitionsResynced := false + alarmDefinitions, ok := r.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID] + if !ok { + slog.Info("Alarm dictionary ID not found in cache", "alarmDictionaryID", alarmDictionaryID) + + // Try to fetch it from the server + alarmDictionary, err := r.getAlarmDictionary(context.Background(), alarmDictionaryID) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to fetch alarm dictionary - alarm Dictionary ID: %w", err) + } + + definitionsResynced = true + alarmDefinitions = getAlarmDefinitionsFromAlarmDictionary(alarmDictionary) + r.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID] = alarmDefinitions + slog.Info("Mapping alarm dictionary ID to alarm definitions", "alarmDictionaryID", alarmDictionaryID) + } + + uniqueAlarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: name, + Severity: severity, + } + + alarmDefinitionID, ok := alarmDefinitions[uniqueAlarmDefinitionIdentifier] + if !ok { + if !definitionsResynced { + // Resync definitions and try again. It is possible that cache is not up to date + slog.Debug("Resynced alarm definitions", "alarmDictionaryID", alarmDictionaryID, "uniqueAlarmDefinitionIdentifier", uniqueAlarmDefinitionIdentifier) + + alarmDictionary, err := r.getAlarmDictionary(context.Background(), alarmDictionaryID) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to fetch alarm dictionary - alarm Dictionary ID: %w", err) + } + + alarmDefinitions = getAlarmDefinitionsFromAlarmDictionary(alarmDictionary) + r.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID] = alarmDefinitions + slog.Info("Mapping alarm dictionary ID to alarm definitions", "alarmDictionaryID", alarmDictionaryID) + + alarmDefinitionID, ok = alarmDefinitions[uniqueAlarmDefinitionIdentifier] + if ok { + return alarmDefinitionID, nil + } + } + + return uuid.Nil, fmt.Errorf("failed to find alarm definition ID for unique identifier: %v", uniqueAlarmDefinitionIdentifier) + } + + return alarmDefinitionID, nil +} + +// ReSync starts a resync for the cluster server +func (r *ClusterServer) ReSync(ctx context.Context) { + slog.Info("Starting resync for ClusterServer") + + go func() { + for { + select { + case <-ctx.Done(): + slog.Info("Stopping resync for ClusterServer") + return + case <-time.After(resyncInterval): + slog.Info("Resyncing ClusterServer") + if err := r.FetchAll(ctx); err != nil { + slog.Error("Failed to resync ClusterServer", "error", err) + } + } + } + }() + +} + +// getNodeClusters lists all node clusters +func (r *ClusterServer) getNodeClusters(ctx context.Context) ([]NodeCluster, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) + defer cancel() + + // TODO: use filters to only request the clusterTypeID field + resp, err := r.client.GetNodeClustersWithResponse(ctxWithTimeout, nil) + if err != nil { + return nil, fmt.Errorf("failed to execute Get operation: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) + } + + slog.Info("Got node clusters", "count", len(*resp.JSON200)) + return *resp.JSON200, nil +} + +// getNodeCluster gets a node cluster by ID +func (r *ClusterServer) getNodeCluster(ctx context.Context, nodeClusterID uuid.UUID) (NodeCluster, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.SingleRequestTimeout) + defer cancel() + + // TODO: use filters to only request the clusterTypeID field + resp, err := r.client.GetNodeClusterWithResponse(ctxWithTimeout, nodeClusterID) + if err != nil { + return NodeCluster{}, fmt.Errorf("failed to execute Get operation: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return NodeCluster{}, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) + } + + slog.Info("Got node cluster", "nodeClusterID", nodeClusterID) + return *resp.JSON200, nil +} + +// getNodeClusterTypes lists all node cluster types +func (r *ClusterServer) getNodeClusterTypes(ctx context.Context) ([]NodeClusterType, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) + defer cancel() + + // TODO: use filters to only request the extensions field or the alarmDictionaryID once it is added + resp, err := r.client.GetNodeClusterTypesWithResponse(ctxWithTimeout, nil) + if err != nil { + return nil, fmt.Errorf("failed to execute Get operation: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) + } + + slog.Info("Got node cluster types", "count", len(*resp.JSON200)) + return *resp.JSON200, nil +} + +// getNodeClusterType gets a node cluster type by ID +func (r *ClusterServer) getNodeClusterType(ctx context.Context, nodeClusterTypeID uuid.UUID) (NodeClusterType, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.SingleRequestTimeout) + defer cancel() + + // TODO: use filters to only request the extensions field or the alarmDictionaryID once it is added + resp, err := r.client.GetNodeClusterTypeWithResponse(ctxWithTimeout, nodeClusterTypeID) + if err != nil { + return NodeClusterType{}, fmt.Errorf("failed to execute Get operation: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return NodeClusterType{}, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) + } + + slog.Info("Got node cluster type", "nodeClusterTypeID", nodeClusterTypeID) + return *resp.JSON200, nil +} + +// getAlarmDictionaries lists all alarm dictionaries +func (r *ClusterServer) getAlarmDictionaries(ctx context.Context) ([]AlarmDictionary, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) + defer cancel() + + // TODO: use filters to only request the definition field + resp, err := r.client.GetAlarmDictionariesWithResponse(ctxWithTimeout, nil) + if err != nil { + return nil, fmt.Errorf("failed to execute Get operation: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return nil, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) + } + + slog.Info("Got alarm dictionaries", "count", len(*resp.JSON200)) + return *resp.JSON200, nil +} + +// GetAlarmDictionary gets an alarm dictionary by ID +func (r *ClusterServer) getAlarmDictionary(ctx context.Context, alarmDictionaryID uuid.UUID) (AlarmDictionary, error) { + ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.SingleRequestTimeout) + defer cancel() + + // TODO: use filters to only request the definition field + resp, err := r.client.GetAlarmDictionaryWithResponse(ctxWithTimeout, alarmDictionaryID) + if err != nil { + return AlarmDictionary{}, fmt.Errorf("failed to execute Get operation: %w", err) + } + + if resp.StatusCode() != http.StatusOK { + return AlarmDictionary{}, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) + } + + slog.Info("Got alarm dictionary", "alarmDictionaryID", alarmDictionaryID) + return *resp.JSON200, nil +} + +// buildNodeClusterIDToNodeClusterTypeID builds the mapping of node cluster ID to node cluster type ID +func (r *ClusterServer) buildNodeClusterIDToNodeClusterTypeID(nodeClusters []NodeCluster) map[uuid.UUID]uuid.UUID { + mapping := make(map[uuid.UUID]uuid.UUID) + for _, nodeCluster := range nodeClusters { + mapping[nodeCluster.NodeClusterId] = nodeCluster.NodeClusterTypeId + slog.Info("Mapping node cluster ID to node cluster type ID", "nodeClusterID", nodeCluster.NodeClusterId, "nodeClusterTypeID", nodeCluster.NodeClusterTypeId) + } + + return mapping +} + +// buildNodeClusterTypeIDToAlarmDictionaryID builds the mapping of node cluster type ID to alarm dictionary ID +func (r *ClusterServer) buildNodeClusterTypeIDToAlarmDictionaryID(nodeClusterTypes []NodeClusterType) map[uuid.UUID]uuid.UUID { + mapping := make(map[uuid.UUID]uuid.UUID) + for _, nodeClusterType := range nodeClusterTypes { + alarmDictionaryID, err := getAlarmDictionaryIDFromNodeClusterType(nodeClusterType) + if err != nil { + slog.Error("Failed to get alarm dictionary ID from node cluster type", "nodeClusterTypeID", nodeClusterType.NodeClusterTypeId, "error", err) + continue + } + + mapping[nodeClusterType.NodeClusterTypeId] = alarmDictionaryID + slog.Info("Mapping node cluster type ID to alarm dictionary ID", "nodeClusterTypeID", nodeClusterType.NodeClusterTypeId, "alarmDictionaryID", alarmDictionaryID) + } + + return mapping +} + +// buildAlarmDictionaryIDToAlarmDefinitions builds the mapping of alarm dictionary ID to alarm definitions +func (r *ClusterServer) buildAlarmDictionaryIDToAlarmDefinitions(dictionaries []AlarmDictionary) map[uuid.UUID]AlarmDefinition { + mapping := make(map[uuid.UUID]AlarmDefinition) + for _, dictionary := range dictionaries { + mapping[dictionary.AlarmDictionaryId] = getAlarmDefinitionsFromAlarmDictionary(dictionary) + slog.Info("Mapping alarm dictionary ID to alarm definitions", "alarmDictionaryID", dictionary.AlarmDictionaryId) + } + + return mapping +} + +// getAlarmDictionaryIDFromNodeClusterType gets the alarm dictionary ID from a node cluster type +func getAlarmDictionaryIDFromNodeClusterType(nodeClusterType NodeClusterType) (uuid.UUID, error) { + if nodeClusterType.Extensions == nil { + return uuid.Nil, fmt.Errorf("node cluster type has no extensions") + } + + alarmDictionaryIDString, ok := (*nodeClusterType.Extensions)[utils.ClusterAlarmDictionaryIDExtension].(string) + if !ok { + return uuid.Nil, fmt.Errorf("node cluster type has no alarm dictionary ID") + } + + alarmDictionaryID, err := uuid.Parse(alarmDictionaryIDString) + if err != nil { + return uuid.Nil, fmt.Errorf("failed to parse alarm dictionary ID: %w", err) + } + + return alarmDictionaryID, nil +} + +// getAlarmDefinitionsFromAlarmDictionary gets the alarm definitions from an alarm dictionary +func getAlarmDefinitionsFromAlarmDictionary(dictionary AlarmDictionary) AlarmDefinition { + alarmDefinitions := make(AlarmDefinition) + for _, definition := range dictionary.AlarmDefinition { + if definition.AlarmAdditionalFields == nil { + slog.Error("Alarm definition has no additional fields", "alarmDefinitionID", definition.AlarmDefinitionId) + continue + } + + severity, ok := (*definition.AlarmAdditionalFields)[utils.AlarmDefinitionSeverityField].(string) + if !ok { + // It should have one, even if it is empty + slog.Error("Alarm definition has no severity", "alarmDefinitionID", definition.AlarmDefinitionId) + continue + } + + uniqueIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: definition.AlarmName, + Severity: severity, + } + + alarmDefinitions[uniqueIdentifier] = definition.AlarmDefinitionId + } + + slog.Debug("Got alarm definitions", "count", len(alarmDefinitions), "alarmDictionaryID", dictionary.AlarmDictionaryId) + return alarmDefinitions +} diff --git a/internal/service/alarms/internal/infrastructure/clusterserver/clusterserver.go b/internal/service/alarms/internal/infrastructure/clusterserver/clusterserver.go deleted file mode 100644 index 1842609e..00000000 --- a/internal/service/alarms/internal/infrastructure/clusterserver/clusterserver.go +++ /dev/null @@ -1,193 +0,0 @@ -package clusterserver - -import ( - "context" - "fmt" - "log/slog" - "net/http" - "os" - "sync" - - "github.com/google/uuid" - "k8s.io/client-go/transport" - - "github.com/openshift-kni/oran-o2ims/internal/controllers/utils" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver/generated" - "github.com/openshift-kni/oran-o2ims/internal/service/common/clients" -) - -const ( - Name = "Cluster" - - clusterServerURLEnvName = "CLUSTER_SERVER_URL" - tokenPathEnvName = "TOKEN_PATH" -) - -type NodeCluster = generated.NodeCluster -type NodeClusterType = generated.NodeClusterType - -type ClusterServer struct { - client *generated.ClientWithResponses - nodeClusters *[]NodeCluster - nodeClusterTypes *[]NodeClusterType - clusterIDToResourceTypeID map[uuid.UUID]uuid.UUID - - sync.Mutex -} - -// Name returns the name of the client -func (r *ClusterServer) Name() string { - return Name -} - -// Setup setups a new client for the cluster server -func (r *ClusterServer) Setup() error { - slog.Info("Creating ClusterServer client") - - url := utils.GetServiceURL(utils.InventoryClusterServerName) - - // Use for local development - clusterServerURL := os.Getenv(clusterServerURLEnvName) - if clusterServerURL != "" { - url = clusterServerURL - } - - // Set up transport - tr, err := utils.GetDefaultBackendTransport() - if err != nil { - return fmt.Errorf("failed to create http transport: %w", err) - } - - hc := http.Client{Transport: tr} - - tokenPath := utils.DefaultBackendTokenFile - - // Use for local development - path := os.Getenv(tokenPathEnvName) - if path != "" { - tokenPath = path - } - - // Create a request editor that uses a cached token source capable of re-reading from file to pickup changes - // as our token is renewed. - editor := clients.AuthorizationEditor{ - Source: transport.NewCachedFileTokenSource(tokenPath), - } - c, err := generated.NewClientWithResponses(url, generated.WithHTTPClient(&hc), generated.WithRequestEditorFn(editor.Editor)) - if err != nil { - return fmt.Errorf("failed to create client: %w", err) - } - - r.client = c - return nil -} - -// FetchAll fetches all necessary data from the cluster server -func (r *ClusterServer) FetchAll(ctx context.Context) error { - slog.Info("Getting all objects from the cluster server") - - // List node clusters - nodeClusters, err := r.getNodeClusters(ctx) - if err != nil { - return fmt.Errorf("failed to get node clusters: %w", err) - } - if nodeClusters == nil { - return fmt.Errorf("no node clusters found: %w", err) - } - - // List node cluster types - nodeClusterTypes, err := r.getNodeClusterTypes(ctx) - if err != nil { - return fmt.Errorf("failed to get node cluster types: %w", err) - } - if nodeClusterTypes == nil { - return fmt.Errorf("no node cluster types found: %w", err) - } - - r.Lock() - defer r.Unlock() - - r.nodeClusters = nodeClusters - r.nodeClusterTypes = nodeClusterTypes - - r.clusterResourceTypeMapping() - - return nil -} - -// getNodeClusters lists all node clusters -func (r *ClusterServer) getNodeClusters(ctx context.Context) (*[]NodeCluster, error) { - ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) - defer cancel() - - resp, err := r.client.GetNodeClustersWithResponse(ctxWithTimeout, nil) - if err != nil { - return nil, fmt.Errorf("failed to execute Get operation: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) - } - - slog.Info("Got node clusters", "count", len(*resp.JSON200)) - - return resp.JSON200, nil -} - -// getNodeClusterTypes lists all node cluster types -func (r *ClusterServer) getNodeClusterTypes(ctx context.Context) (*[]NodeClusterType, error) { - ctxWithTimeout, cancel := context.WithTimeout(ctx, clients.ListRequestTimeout) - defer cancel() - - resp, err := r.client.GetNodeClusterTypesWithResponse(ctxWithTimeout, nil) - if err != nil { - return nil, fmt.Errorf("failed to execute Get operation: %w", err) - } - - if resp.StatusCode() != http.StatusOK { - return nil, fmt.Errorf("status code different from 200 OK: %s", resp.Status()) - } - - slog.Info("Got node cluster types", "count", len(*resp.JSON200)) - - return resp.JSON200, nil -} - -// clusterResourceTypeMapping map cluster ID with objectType ID for faster lookup during Caas alerts -func (r *ClusterServer) clusterResourceTypeMapping() { - mapping := make(map[uuid.UUID]uuid.UUID) - for _, cluster := range *r.nodeClusters { - mapping[cluster.NodeClusterId] = cluster.NodeClusterTypeId - slog.Info("Mapping cluster ID to resource type ID", "ClusterID", cluster.NodeClusterId, "NodeClusterTypeId", cluster.NodeClusterTypeId) - } - - r.clusterIDToResourceTypeID = mapping -} - -// GetNodeClusterTypes returns a copy of the node cluster types -func (r *ClusterServer) GetNodeClusterTypes() []NodeClusterType { - r.Lock() - defer r.Unlock() - - if r.nodeClusterTypes == nil { - return nil - } - - nodeClusterTypesCopy := make([]NodeClusterType, len(*r.nodeClusterTypes)) - copy(nodeClusterTypesCopy, *r.nodeClusterTypes) - - return nodeClusterTypesCopy -} - -// GetClusterIDToResourceTypeID returns a copy of the cluster ID to resource type ID mapping -func (r *ClusterServer) GetClusterIDToResourceTypeID() map[uuid.UUID]uuid.UUID { - r.Lock() - defer r.Unlock() - - c := make(map[uuid.UUID]uuid.UUID) - for k, v := range r.clusterIDToResourceTypeID { - c[k] = v - } - - return c -} diff --git a/internal/service/alarms/internal/infrastructure/clusterserver/generated/generate.go b/internal/service/alarms/internal/infrastructure/clusterserver/generated/generate.go index 5788db2e..14f3f931 100644 --- a/internal/service/alarms/internal/infrastructure/clusterserver/generated/generate.go +++ b/internal/service/alarms/internal/infrastructure/clusterserver/generated/generate.go @@ -1,3 +1,4 @@ package generated //go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -config oapi-codegen.yaml ../../../../../cluster/api/openapi.yaml +//go:generate mockgen -source=client.gen.go -destination=mock_generated/mock_client.gen.go -package=mock_generated diff --git a/internal/service/alarms/internal/infrastructure/clusterserver/generated/mock_generated/mock_client.gen.go b/internal/service/alarms/internal/infrastructure/clusterserver/generated/mock_generated/mock_client.gen.go new file mode 100644 index 00000000..ee8e4432 --- /dev/null +++ b/internal/service/alarms/internal/infrastructure/clusterserver/generated/mock_generated/mock_client.gen.go @@ -0,0 +1,824 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: client.gen.go +// +// Generated by this command: +// +// mockgen -source=client.gen.go -destination=mock_generated/mock_client.gen.go -package=mock_generated +// +// Package mock_generated is a generated GoMock package. +package mock_generated + +import ( + context "context" + io "io" + http "net/http" + reflect "reflect" + + generated "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver/generated" + generated0 "github.com/openshift-kni/oran-o2ims/internal/service/common/api/generated" + gomock "go.uber.org/mock/gomock" +) + +// MockHttpRequestDoer is a mock of HttpRequestDoer interface. +type MockHttpRequestDoer struct { + ctrl *gomock.Controller + recorder *MockHttpRequestDoerMockRecorder +} + +// MockHttpRequestDoerMockRecorder is the mock recorder for MockHttpRequestDoer. +type MockHttpRequestDoerMockRecorder struct { + mock *MockHttpRequestDoer +} + +// NewMockHttpRequestDoer creates a new mock instance. +func NewMockHttpRequestDoer(ctrl *gomock.Controller) *MockHttpRequestDoer { + mock := &MockHttpRequestDoer{ctrl: ctrl} + mock.recorder = &MockHttpRequestDoerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockHttpRequestDoer) EXPECT() *MockHttpRequestDoerMockRecorder { + return m.recorder +} + +// Do mocks base method. +func (m *MockHttpRequestDoer) Do(req *http.Request) (*http.Response, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Do", req) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Do indicates an expected call of Do. +func (mr *MockHttpRequestDoerMockRecorder) Do(req any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Do", reflect.TypeOf((*MockHttpRequestDoer)(nil).Do), req) +} + +// MockClientInterface is a mock of ClientInterface interface. +type MockClientInterface struct { + ctrl *gomock.Controller + recorder *MockClientInterfaceMockRecorder +} + +// MockClientInterfaceMockRecorder is the mock recorder for MockClientInterface. +type MockClientInterfaceMockRecorder struct { + mock *MockClientInterface +} + +// NewMockClientInterface creates a new mock instance. +func NewMockClientInterface(ctrl *gomock.Controller) *MockClientInterface { + mock := &MockClientInterface{ctrl: ctrl} + mock.recorder = &MockClientInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClientInterface) EXPECT() *MockClientInterfaceMockRecorder { + return m.recorder +} + +// CreateSubscription mocks base method. +func (m *MockClientInterface) CreateSubscription(ctx context.Context, body generated.CreateSubscriptionJSONRequestBody, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateSubscription", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSubscription indicates an expected call of CreateSubscription. +func (mr *MockClientInterfaceMockRecorder) CreateSubscription(ctx, body any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscription", reflect.TypeOf((*MockClientInterface)(nil).CreateSubscription), varargs...) +} + +// CreateSubscriptionWithBody mocks base method. +func (m *MockClientInterface) CreateSubscriptionWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, contentType, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateSubscriptionWithBody", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSubscriptionWithBody indicates an expected call of CreateSubscriptionWithBody. +func (mr *MockClientInterfaceMockRecorder) CreateSubscriptionWithBody(ctx, contentType, body any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, contentType, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscriptionWithBody", reflect.TypeOf((*MockClientInterface)(nil).CreateSubscriptionWithBody), varargs...) +} + +// DeleteSubscription mocks base method. +func (m *MockClientInterface) DeleteSubscription(ctx context.Context, subscriptionId generated.SubscriptionId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, subscriptionId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteSubscription", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteSubscription indicates an expected call of DeleteSubscription. +func (mr *MockClientInterfaceMockRecorder) DeleteSubscription(ctx, subscriptionId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, subscriptionId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscription", reflect.TypeOf((*MockClientInterface)(nil).DeleteSubscription), varargs...) +} + +// GetAlarmDictionaries mocks base method. +func (m *MockClientInterface) GetAlarmDictionaries(ctx context.Context, params *generated.GetAlarmDictionariesParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAlarmDictionaries", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAlarmDictionaries indicates an expected call of GetAlarmDictionaries. +func (mr *MockClientInterfaceMockRecorder) GetAlarmDictionaries(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlarmDictionaries", reflect.TypeOf((*MockClientInterface)(nil).GetAlarmDictionaries), varargs...) +} + +// GetAlarmDictionary mocks base method. +func (m *MockClientInterface) GetAlarmDictionary(ctx context.Context, alarmDictionaryId generated0.AlarmDictionaryId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, alarmDictionaryId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAlarmDictionary", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAlarmDictionary indicates an expected call of GetAlarmDictionary. +func (mr *MockClientInterfaceMockRecorder) GetAlarmDictionary(ctx, alarmDictionaryId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, alarmDictionaryId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlarmDictionary", reflect.TypeOf((*MockClientInterface)(nil).GetAlarmDictionary), varargs...) +} + +// GetAllVersions mocks base method. +func (m *MockClientInterface) GetAllVersions(ctx context.Context, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAllVersions", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllVersions indicates an expected call of GetAllVersions. +func (mr *MockClientInterfaceMockRecorder) GetAllVersions(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllVersions", reflect.TypeOf((*MockClientInterface)(nil).GetAllVersions), varargs...) +} + +// GetClusterResource mocks base method. +func (m *MockClientInterface) GetClusterResource(ctx context.Context, clusterResourceId generated.ClusterResourceId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, clusterResourceId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResource", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResource indicates an expected call of GetClusterResource. +func (mr *MockClientInterfaceMockRecorder) GetClusterResource(ctx, clusterResourceId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, clusterResourceId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResource", reflect.TypeOf((*MockClientInterface)(nil).GetClusterResource), varargs...) +} + +// GetClusterResourceType mocks base method. +func (m *MockClientInterface) GetClusterResourceType(ctx context.Context, clusterResourceTypeId generated.ClusterResourceTypeId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, clusterResourceTypeId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResourceType", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResourceType indicates an expected call of GetClusterResourceType. +func (mr *MockClientInterfaceMockRecorder) GetClusterResourceType(ctx, clusterResourceTypeId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, clusterResourceTypeId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResourceType", reflect.TypeOf((*MockClientInterface)(nil).GetClusterResourceType), varargs...) +} + +// GetClusterResourceTypes mocks base method. +func (m *MockClientInterface) GetClusterResourceTypes(ctx context.Context, params *generated.GetClusterResourceTypesParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResourceTypes", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResourceTypes indicates an expected call of GetClusterResourceTypes. +func (mr *MockClientInterfaceMockRecorder) GetClusterResourceTypes(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResourceTypes", reflect.TypeOf((*MockClientInterface)(nil).GetClusterResourceTypes), varargs...) +} + +// GetClusterResources mocks base method. +func (m *MockClientInterface) GetClusterResources(ctx context.Context, params *generated.GetClusterResourcesParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResources", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResources indicates an expected call of GetClusterResources. +func (mr *MockClientInterfaceMockRecorder) GetClusterResources(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResources", reflect.TypeOf((*MockClientInterface)(nil).GetClusterResources), varargs...) +} + +// GetMinorVersions mocks base method. +func (m *MockClientInterface) GetMinorVersions(ctx context.Context, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetMinorVersions", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMinorVersions indicates an expected call of GetMinorVersions. +func (mr *MockClientInterfaceMockRecorder) GetMinorVersions(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinorVersions", reflect.TypeOf((*MockClientInterface)(nil).GetMinorVersions), varargs...) +} + +// GetNodeCluster mocks base method. +func (m *MockClientInterface) GetNodeCluster(ctx context.Context, nodeClusterId generated.NodeClusterId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, nodeClusterId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeCluster", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeCluster indicates an expected call of GetNodeCluster. +func (mr *MockClientInterfaceMockRecorder) GetNodeCluster(ctx, nodeClusterId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, nodeClusterId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeCluster", reflect.TypeOf((*MockClientInterface)(nil).GetNodeCluster), varargs...) +} + +// GetNodeClusterType mocks base method. +func (m *MockClientInterface) GetNodeClusterType(ctx context.Context, nodeClusterTypeId generated.NodeClusterTypeId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, nodeClusterTypeId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterType", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterType indicates an expected call of GetNodeClusterType. +func (mr *MockClientInterfaceMockRecorder) GetNodeClusterType(ctx, nodeClusterTypeId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, nodeClusterTypeId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterType", reflect.TypeOf((*MockClientInterface)(nil).GetNodeClusterType), varargs...) +} + +// GetNodeClusterTypeAlarmDictionary mocks base method. +func (m *MockClientInterface) GetNodeClusterTypeAlarmDictionary(ctx context.Context, nodeClusterTypeId generated.NodeClusterTypeId, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, nodeClusterTypeId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterTypeAlarmDictionary", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterTypeAlarmDictionary indicates an expected call of GetNodeClusterTypeAlarmDictionary. +func (mr *MockClientInterfaceMockRecorder) GetNodeClusterTypeAlarmDictionary(ctx, nodeClusterTypeId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, nodeClusterTypeId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterTypeAlarmDictionary", reflect.TypeOf((*MockClientInterface)(nil).GetNodeClusterTypeAlarmDictionary), varargs...) +} + +// GetNodeClusterTypes mocks base method. +func (m *MockClientInterface) GetNodeClusterTypes(ctx context.Context, params *generated.GetNodeClusterTypesParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterTypes", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterTypes indicates an expected call of GetNodeClusterTypes. +func (mr *MockClientInterfaceMockRecorder) GetNodeClusterTypes(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterTypes", reflect.TypeOf((*MockClientInterface)(nil).GetNodeClusterTypes), varargs...) +} + +// GetNodeClusters mocks base method. +func (m *MockClientInterface) GetNodeClusters(ctx context.Context, params *generated.GetNodeClustersParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusters", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusters indicates an expected call of GetNodeClusters. +func (mr *MockClientInterfaceMockRecorder) GetNodeClusters(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusters", reflect.TypeOf((*MockClientInterface)(nil).GetNodeClusters), varargs...) +} + +// GetSubscription mocks base method. +func (m *MockClientInterface) GetSubscription(ctx context.Context, subscriptionId generated.SubscriptionId, params *generated.GetSubscriptionParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, subscriptionId, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSubscription", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubscription indicates an expected call of GetSubscription. +func (mr *MockClientInterfaceMockRecorder) GetSubscription(ctx, subscriptionId, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, subscriptionId, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscription", reflect.TypeOf((*MockClientInterface)(nil).GetSubscription), varargs...) +} + +// GetSubscriptions mocks base method. +func (m *MockClientInterface) GetSubscriptions(ctx context.Context, params *generated.GetSubscriptionsParams, reqEditors ...generated.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSubscriptions", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubscriptions indicates an expected call of GetSubscriptions. +func (mr *MockClientInterfaceMockRecorder) GetSubscriptions(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptions", reflect.TypeOf((*MockClientInterface)(nil).GetSubscriptions), varargs...) +} + +// MockClientWithResponsesInterface is a mock of ClientWithResponsesInterface interface. +type MockClientWithResponsesInterface struct { + ctrl *gomock.Controller + recorder *MockClientWithResponsesInterfaceMockRecorder +} + +// MockClientWithResponsesInterfaceMockRecorder is the mock recorder for MockClientWithResponsesInterface. +type MockClientWithResponsesInterfaceMockRecorder struct { + mock *MockClientWithResponsesInterface +} + +// NewMockClientWithResponsesInterface creates a new mock instance. +func NewMockClientWithResponsesInterface(ctrl *gomock.Controller) *MockClientWithResponsesInterface { + mock := &MockClientWithResponsesInterface{ctrl: ctrl} + mock.recorder = &MockClientWithResponsesInterfaceMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockClientWithResponsesInterface) EXPECT() *MockClientWithResponsesInterfaceMockRecorder { + return m.recorder +} + +// CreateSubscriptionWithBodyWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) CreateSubscriptionWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...generated.RequestEditorFn) (*generated.CreateSubscriptionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, contentType, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateSubscriptionWithBodyWithResponse", varargs...) + ret0, _ := ret[0].(*generated.CreateSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSubscriptionWithBodyWithResponse indicates an expected call of CreateSubscriptionWithBodyWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) CreateSubscriptionWithBodyWithResponse(ctx, contentType, body any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, contentType, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscriptionWithBodyWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).CreateSubscriptionWithBodyWithResponse), varargs...) +} + +// CreateSubscriptionWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) CreateSubscriptionWithResponse(ctx context.Context, body generated.CreateSubscriptionJSONRequestBody, reqEditors ...generated.RequestEditorFn) (*generated.CreateSubscriptionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateSubscriptionWithResponse", varargs...) + ret0, _ := ret[0].(*generated.CreateSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSubscriptionWithResponse indicates an expected call of CreateSubscriptionWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) CreateSubscriptionWithResponse(ctx, body any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSubscriptionWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).CreateSubscriptionWithResponse), varargs...) +} + +// DeleteSubscriptionWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) DeleteSubscriptionWithResponse(ctx context.Context, subscriptionId generated.SubscriptionId, reqEditors ...generated.RequestEditorFn) (*generated.DeleteSubscriptionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, subscriptionId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteSubscriptionWithResponse", varargs...) + ret0, _ := ret[0].(*generated.DeleteSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteSubscriptionWithResponse indicates an expected call of DeleteSubscriptionWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) DeleteSubscriptionWithResponse(ctx, subscriptionId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, subscriptionId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSubscriptionWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).DeleteSubscriptionWithResponse), varargs...) +} + +// GetAlarmDictionariesWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetAlarmDictionariesWithResponse(ctx context.Context, params *generated.GetAlarmDictionariesParams, reqEditors ...generated.RequestEditorFn) (*generated.GetAlarmDictionariesResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAlarmDictionariesWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetAlarmDictionariesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAlarmDictionariesWithResponse indicates an expected call of GetAlarmDictionariesWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetAlarmDictionariesWithResponse(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlarmDictionariesWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetAlarmDictionariesWithResponse), varargs...) +} + +// GetAlarmDictionaryWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetAlarmDictionaryWithResponse(ctx context.Context, alarmDictionaryId generated0.AlarmDictionaryId, reqEditors ...generated.RequestEditorFn) (*generated.GetAlarmDictionaryResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, alarmDictionaryId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAlarmDictionaryWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetAlarmDictionaryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAlarmDictionaryWithResponse indicates an expected call of GetAlarmDictionaryWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetAlarmDictionaryWithResponse(ctx, alarmDictionaryId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, alarmDictionaryId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAlarmDictionaryWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetAlarmDictionaryWithResponse), varargs...) +} + +// GetAllVersionsWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetAllVersionsWithResponse(ctx context.Context, reqEditors ...generated.RequestEditorFn) (*generated.GetAllVersionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetAllVersionsWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetAllVersionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAllVersionsWithResponse indicates an expected call of GetAllVersionsWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetAllVersionsWithResponse(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAllVersionsWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetAllVersionsWithResponse), varargs...) +} + +// GetClusterResourceTypeWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetClusterResourceTypeWithResponse(ctx context.Context, clusterResourceTypeId generated.ClusterResourceTypeId, reqEditors ...generated.RequestEditorFn) (*generated.GetClusterResourceTypeResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, clusterResourceTypeId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResourceTypeWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetClusterResourceTypeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResourceTypeWithResponse indicates an expected call of GetClusterResourceTypeWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetClusterResourceTypeWithResponse(ctx, clusterResourceTypeId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, clusterResourceTypeId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResourceTypeWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetClusterResourceTypeWithResponse), varargs...) +} + +// GetClusterResourceTypesWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetClusterResourceTypesWithResponse(ctx context.Context, params *generated.GetClusterResourceTypesParams, reqEditors ...generated.RequestEditorFn) (*generated.GetClusterResourceTypesResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResourceTypesWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetClusterResourceTypesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResourceTypesWithResponse indicates an expected call of GetClusterResourceTypesWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetClusterResourceTypesWithResponse(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResourceTypesWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetClusterResourceTypesWithResponse), varargs...) +} + +// GetClusterResourceWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetClusterResourceWithResponse(ctx context.Context, clusterResourceId generated.ClusterResourceId, reqEditors ...generated.RequestEditorFn) (*generated.GetClusterResourceResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, clusterResourceId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResourceWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetClusterResourceResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResourceWithResponse indicates an expected call of GetClusterResourceWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetClusterResourceWithResponse(ctx, clusterResourceId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, clusterResourceId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResourceWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetClusterResourceWithResponse), varargs...) +} + +// GetClusterResourcesWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetClusterResourcesWithResponse(ctx context.Context, params *generated.GetClusterResourcesParams, reqEditors ...generated.RequestEditorFn) (*generated.GetClusterResourcesResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetClusterResourcesWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetClusterResourcesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetClusterResourcesWithResponse indicates an expected call of GetClusterResourcesWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetClusterResourcesWithResponse(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClusterResourcesWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetClusterResourcesWithResponse), varargs...) +} + +// GetMinorVersionsWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetMinorVersionsWithResponse(ctx context.Context, reqEditors ...generated.RequestEditorFn) (*generated.GetMinorVersionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetMinorVersionsWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetMinorVersionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetMinorVersionsWithResponse indicates an expected call of GetMinorVersionsWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetMinorVersionsWithResponse(ctx any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinorVersionsWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetMinorVersionsWithResponse), varargs...) +} + +// GetNodeClusterTypeAlarmDictionaryWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetNodeClusterTypeAlarmDictionaryWithResponse(ctx context.Context, nodeClusterTypeId generated.NodeClusterTypeId, reqEditors ...generated.RequestEditorFn) (*generated.GetNodeClusterTypeAlarmDictionaryResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, nodeClusterTypeId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterTypeAlarmDictionaryWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetNodeClusterTypeAlarmDictionaryResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterTypeAlarmDictionaryWithResponse indicates an expected call of GetNodeClusterTypeAlarmDictionaryWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetNodeClusterTypeAlarmDictionaryWithResponse(ctx, nodeClusterTypeId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, nodeClusterTypeId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterTypeAlarmDictionaryWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetNodeClusterTypeAlarmDictionaryWithResponse), varargs...) +} + +// GetNodeClusterTypeWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetNodeClusterTypeWithResponse(ctx context.Context, nodeClusterTypeId generated.NodeClusterTypeId, reqEditors ...generated.RequestEditorFn) (*generated.GetNodeClusterTypeResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, nodeClusterTypeId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterTypeWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetNodeClusterTypeResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterTypeWithResponse indicates an expected call of GetNodeClusterTypeWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetNodeClusterTypeWithResponse(ctx, nodeClusterTypeId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, nodeClusterTypeId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterTypeWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetNodeClusterTypeWithResponse), varargs...) +} + +// GetNodeClusterTypesWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetNodeClusterTypesWithResponse(ctx context.Context, params *generated.GetNodeClusterTypesParams, reqEditors ...generated.RequestEditorFn) (*generated.GetNodeClusterTypesResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterTypesWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetNodeClusterTypesResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterTypesWithResponse indicates an expected call of GetNodeClusterTypesWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetNodeClusterTypesWithResponse(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterTypesWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetNodeClusterTypesWithResponse), varargs...) +} + +// GetNodeClusterWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetNodeClusterWithResponse(ctx context.Context, nodeClusterId generated.NodeClusterId, reqEditors ...generated.RequestEditorFn) (*generated.GetNodeClusterResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, nodeClusterId} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClusterWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetNodeClusterResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClusterWithResponse indicates an expected call of GetNodeClusterWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetNodeClusterWithResponse(ctx, nodeClusterId any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, nodeClusterId}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClusterWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetNodeClusterWithResponse), varargs...) +} + +// GetNodeClustersWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetNodeClustersWithResponse(ctx context.Context, params *generated.GetNodeClustersParams, reqEditors ...generated.RequestEditorFn) (*generated.GetNodeClustersResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetNodeClustersWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetNodeClustersResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetNodeClustersWithResponse indicates an expected call of GetNodeClustersWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetNodeClustersWithResponse(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetNodeClustersWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetNodeClustersWithResponse), varargs...) +} + +// GetSubscriptionWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetSubscriptionWithResponse(ctx context.Context, subscriptionId generated.SubscriptionId, params *generated.GetSubscriptionParams, reqEditors ...generated.RequestEditorFn) (*generated.GetSubscriptionResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, subscriptionId, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSubscriptionWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetSubscriptionResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubscriptionWithResponse indicates an expected call of GetSubscriptionWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetSubscriptionWithResponse(ctx, subscriptionId, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, subscriptionId, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptionWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetSubscriptionWithResponse), varargs...) +} + +// GetSubscriptionsWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) GetSubscriptionsWithResponse(ctx context.Context, params *generated.GetSubscriptionsParams, reqEditors ...generated.RequestEditorFn) (*generated.GetSubscriptionsResponse, error) { + m.ctrl.T.Helper() + varargs := []any{ctx, params} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "GetSubscriptionsWithResponse", varargs...) + ret0, _ := ret[0].(*generated.GetSubscriptionsResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSubscriptionsWithResponse indicates an expected call of GetSubscriptionsWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) GetSubscriptionsWithResponse(ctx, params any, reqEditors ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, params}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptionsWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).GetSubscriptionsWithResponse), varargs...) +} diff --git a/internal/service/alarms/internal/infrastructure/clusterserver_test.go b/internal/service/alarms/internal/infrastructure/clusterserver_test.go new file mode 100644 index 00000000..0a7044b3 --- /dev/null +++ b/internal/service/alarms/internal/infrastructure/clusterserver_test.go @@ -0,0 +1,475 @@ +package infrastructure + +import ( + "bytes" + "context" + "encoding/json" + "io" + "net/http" + + "github.com/google/uuid" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/mock/gomock" + + "github.com/openshift-kni/oran-o2ims/internal/controllers/utils" + "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver/generated" + "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver/generated/mock_generated" + commonsgenerated "github.com/openshift-kni/oran-o2ims/internal/service/common/api/generated" +) + +type objectIDAssociations struct { + nodeClusterID uuid.UUID + nodeClusterTypeID uuid.UUID + alarmDictionaryID uuid.UUID + + alarmDefinition1ID uuid.UUID + alarmDefinition1Identifier AlarmDefinitionUniqueIdentifier + alarmDefinition2ID uuid.UUID + alarmDefinition2Identifier AlarmDefinitionUniqueIdentifier +} + +var _ = Describe("ClusterServer", func() { + var clusterServer *ClusterServer + var ctrl *gomock.Controller + var mockRepo *mock_generated.MockClientInterface + var ctx context.Context + + BeforeEach(func() { + ctx = context.Background() + ctrl = gomock.NewController(GinkgoT()) + mockRepo = mock_generated.NewMockClientInterface(ctrl) + + clusterServer = &ClusterServer{ + client: &generated.ClientWithResponses{ClientInterface: mockRepo}, + } + }) + + Describe("FetchAll", func() { + It("should fetch data and populate internal maps", func() { + firstAssociation := objectIDAssociations{ + nodeClusterID: uuid.New(), + nodeClusterTypeID: uuid.New(), + alarmDictionaryID: uuid.New(), + alarmDefinition1ID: uuid.New(), + alarmDefinition1Identifier: AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + }, + alarmDefinition2ID: uuid.New(), + alarmDefinition2Identifier: AlarmDefinitionUniqueIdentifier{ + Name: "alarm2", + Severity: "warning", + }, + } + + secondAssociation := objectIDAssociations{ + nodeClusterID: uuid.New(), + nodeClusterTypeID: uuid.New(), + alarmDictionaryID: uuid.New(), + alarmDefinition1ID: uuid.New(), + alarmDefinition1Identifier: AlarmDefinitionUniqueIdentifier{ + Name: "alarm3", + Severity: "critical", + }, + alarmDefinition2ID: uuid.New(), + alarmDefinition2Identifier: AlarmDefinitionUniqueIdentifier{ + Name: "alarm4", + Severity: "warning", + }, + } + + nodeClusters := []generated.NodeCluster{ + { + NodeClusterId: firstAssociation.nodeClusterID, + NodeClusterTypeId: firstAssociation.nodeClusterTypeID, + }, + { + NodeClusterId: secondAssociation.nodeClusterID, + NodeClusterTypeId: secondAssociation.nodeClusterTypeID, + }, + } + body, err := json.Marshal(nodeClusters) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetNodeClusters(gomock.Any(), nil).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + nodeClusterTypes := []generated.NodeClusterType{ + { + NodeClusterTypeId: firstAssociation.nodeClusterTypeID, + Extensions: &map[string]interface{}{ + utils.ClusterAlarmDictionaryIDExtension: firstAssociation.alarmDictionaryID, + }, + }, + { + NodeClusterTypeId: secondAssociation.nodeClusterTypeID, + Extensions: &map[string]interface{}{ + utils.ClusterAlarmDictionaryIDExtension: secondAssociation.alarmDictionaryID, + }, + }, + } + body, err = json.Marshal(nodeClusterTypes) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetNodeClusterTypes(gomock.Any(), nil).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + alarmDictionaries := []commonsgenerated.AlarmDictionary{ + { + AlarmDictionaryId: firstAssociation.alarmDictionaryID, + AlarmDefinition: []commonsgenerated.AlarmDefinition{ + { + AlarmDefinitionId: firstAssociation.alarmDefinition1ID, + AlarmName: firstAssociation.alarmDefinition1Identifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": firstAssociation.alarmDefinition1Identifier.Severity, + }, + }, + { + AlarmDefinitionId: firstAssociation.alarmDefinition2ID, + AlarmName: firstAssociation.alarmDefinition2Identifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": firstAssociation.alarmDefinition2Identifier.Severity, + }, + }, + }, + }, + { + AlarmDictionaryId: secondAssociation.alarmDictionaryID, + AlarmDefinition: []commonsgenerated.AlarmDefinition{ + { + AlarmDefinitionId: secondAssociation.alarmDefinition1ID, + AlarmName: secondAssociation.alarmDefinition1Identifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": secondAssociation.alarmDefinition1Identifier.Severity, + }, + }, + { + AlarmDefinitionId: secondAssociation.alarmDefinition2ID, + AlarmName: secondAssociation.alarmDefinition2Identifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": secondAssociation.alarmDefinition2Identifier.Severity, + }, + }, + }, + }, + } + body, err = json.Marshal(alarmDictionaries) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetAlarmDictionaries(gomock.Any(), nil).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + err = clusterServer.FetchAll(ctx) + Expect(err).To(BeNil()) + + Expect(clusterServer.nodeClusterIDToNodeClusterTypeID).To(HaveLen(2)) + Expect(clusterServer.nodeClusterTypeIDToAlarmDictionaryID).To(HaveLen(2)) + Expect(clusterServer.alarmDictionaryIDToAlarmDefinitions).To(HaveLen(2)) + Expect(clusterServer.alarmDictionaryIDToAlarmDefinitions[firstAssociation.alarmDictionaryID]).To(HaveLen(2)) + Expect(clusterServer.alarmDictionaryIDToAlarmDefinitions[secondAssociation.alarmDictionaryID]).To(HaveLen(2)) + + Expect(clusterServer.nodeClusterIDToNodeClusterTypeID[firstAssociation.nodeClusterID]).To(Equal(firstAssociation.nodeClusterTypeID)) + Expect(clusterServer.nodeClusterIDToNodeClusterTypeID[secondAssociation.nodeClusterID]).To(Equal(secondAssociation.nodeClusterTypeID)) + Expect(clusterServer.nodeClusterTypeIDToAlarmDictionaryID[firstAssociation.nodeClusterTypeID]).To(Equal(firstAssociation.alarmDictionaryID)) + Expect(clusterServer.nodeClusterTypeIDToAlarmDictionaryID[secondAssociation.nodeClusterTypeID]).To(Equal(secondAssociation.alarmDictionaryID)) + Expect(clusterServer.alarmDictionaryIDToAlarmDefinitions[firstAssociation.alarmDictionaryID][firstAssociation.alarmDefinition1Identifier]).To(Equal(firstAssociation.alarmDefinition1ID)) + Expect(clusterServer.alarmDictionaryIDToAlarmDefinitions[firstAssociation.alarmDictionaryID][firstAssociation.alarmDefinition2Identifier]).To(Equal(firstAssociation.alarmDefinition2ID)) + }) + }) + + Describe("GetObjectTypeID", func() { + It("should find the node cluster type ID in cache", func() { + nodeClusterID := uuid.New() + nodeClusterTypeID := uuid.New() + + clusterServer.nodeClusterIDToNodeClusterTypeID = make(map[uuid.UUID]uuid.UUID) + clusterServer.nodeClusterIDToNodeClusterTypeID[nodeClusterID] = nodeClusterTypeID + + id, err := clusterServer.GetObjectTypeID(nodeClusterID) + Expect(err).To(BeNil()) + Expect(id).To(Equal(nodeClusterTypeID)) + }) + + It("should fetch the node cluster type ID from server if not found in cache", func() { + nodeClusterID := uuid.New() + nodeClusterTypeID := uuid.New() + + clusterServer.nodeClusterIDToNodeClusterTypeID = make(map[uuid.UUID]uuid.UUID) + + nodeClusters := generated.NodeCluster{ + NodeClusterId: nodeClusterID, + NodeClusterTypeId: nodeClusterTypeID, + } + body, err := json.Marshal(nodeClusters) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetNodeCluster(gomock.Any(), nodeClusterID).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + id, err := clusterServer.GetObjectTypeID(nodeClusterID) + Expect(err).To(BeNil()) + Expect(id).To(Equal(nodeClusterTypeID)) + Expect(clusterServer.nodeClusterIDToNodeClusterTypeID[nodeClusterID]).To(Equal(nodeClusterTypeID)) + }) + + It("should return an error if node cluster type ID is not in cache nor in server", func() { + nodeClusterID := uuid.New() + + clusterServer.nodeClusterIDToNodeClusterTypeID = make(map[uuid.UUID]uuid.UUID) + + mockRepo.EXPECT().GetNodeCluster(gomock.Any(), nodeClusterID).Return( + &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte{})), + }, nil) + + _, err := clusterServer.GetObjectTypeID(nodeClusterID) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe("GetAlarmDefinitionID", func() { + It("should find the alarm dictionary ID in cache", func() { + nodeClusterTypeID := uuid.New() + alarmDictionaryID := uuid.New() + alarmDefinitionID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + clusterServer.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] = alarmDictionaryID + clusterServer.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + clusterServer.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID] = make(AlarmDefinition) + clusterServer.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID][alarmDefinitionIdentifier] = alarmDefinitionID + + id, err := clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, alarmDefinitionIdentifier.Name, alarmDefinitionIdentifier.Severity) + Expect(err).To(BeNil()) + Expect(id).To(Equal(alarmDefinitionID)) + }) + + It("should succeed when node cluster type ID is not found in cache and server fetch is successful", func() { + nodeClusterTypeID := uuid.New() + alarmDictionaryID := uuid.New() + alarmDefinitionID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + + clusterServer.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + clusterServer.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID] = make(AlarmDefinition) + clusterServer.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID][alarmDefinitionIdentifier] = alarmDefinitionID + + nodeClusterType := generated.NodeClusterType{ + NodeClusterTypeId: nodeClusterTypeID, + Extensions: &map[string]interface{}{ + utils.ClusterAlarmDictionaryIDExtension: alarmDictionaryID, + }, + } + body, err := json.Marshal(nodeClusterType) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetNodeClusterType(gomock.Any(), nodeClusterTypeID).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + id, err := clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, alarmDefinitionIdentifier.Name, alarmDefinitionIdentifier.Severity) + Expect(err).To(BeNil()) + Expect(id).To(Equal(alarmDefinitionID)) + }) + + It("should fail when node cluster type ID is not found in cache nor server", func() { + nodeClusterTypeID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + + mockRepo.EXPECT().GetNodeClusterType(gomock.Any(), nodeClusterTypeID).Return( + &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte{})), + }, nil) + + _, err := clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, alarmDefinitionIdentifier.Name, alarmDefinitionIdentifier.Severity) + Expect(err).To(HaveOccurred()) + }) + + It("should succeed when alarm dictionary ID is not found in cache and server fetch is successful", func() { + nodeClusterTypeID := uuid.New() + alarmDictionaryID := uuid.New() + alarmDefinitionID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + clusterServer.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] = alarmDictionaryID + clusterServer.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + + alarmDictionary := commonsgenerated.AlarmDictionary{ + AlarmDictionaryId: alarmDictionaryID, + AlarmDefinition: []commonsgenerated.AlarmDefinition{ + { + AlarmDefinitionId: alarmDefinitionID, + AlarmName: alarmDefinitionIdentifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": alarmDefinitionIdentifier.Severity, + }, + }, + }, + } + body, err := json.Marshal(alarmDictionary) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetAlarmDictionary(gomock.Any(), alarmDictionaryID).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + id, err := clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, alarmDefinitionIdentifier.Name, alarmDefinitionIdentifier.Severity) + Expect(err).To(BeNil()) + Expect(id).To(Equal(alarmDefinitionID)) + }) + + It("should fail when alarm dictionary ID is not found in cache nor in the server", func() { + nodeClusterTypeID := uuid.New() + alarmDictionaryID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + clusterServer.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] = alarmDictionaryID + clusterServer.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + + mockRepo.EXPECT().GetAlarmDictionary(gomock.Any(), alarmDictionaryID).Return( + &http.Response{ + StatusCode: http.StatusNotFound, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte{})), + }, nil) + + _, err := clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, alarmDefinitionIdentifier.Name, alarmDefinitionIdentifier.Severity) + Expect(err).To(HaveOccurred()) + }) + + It("should succeed when alarm dictionary is in cache but the definition is not. There should a retry that updates the cache", func() { + nodeClusterTypeID := uuid.New() + alarmDictionaryID := uuid.New() + alarmDefinitionID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + clusterServer.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] = alarmDictionaryID + clusterServer.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + clusterServer.alarmDictionaryIDToAlarmDefinitions[alarmDictionaryID] = make(AlarmDefinition) + + alarmDictionary := commonsgenerated.AlarmDictionary{ + AlarmDictionaryId: alarmDictionaryID, + AlarmDefinition: []commonsgenerated.AlarmDefinition{ + { + AlarmDefinitionId: alarmDefinitionID, + AlarmName: alarmDefinitionIdentifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": alarmDefinitionIdentifier.Severity, + }, + }, + }, + } + body, err := json.Marshal(alarmDictionary) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetAlarmDictionary(gomock.Any(), alarmDictionaryID).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + id, err := clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, alarmDefinitionIdentifier.Name, alarmDefinitionIdentifier.Severity) + Expect(err).To(BeNil()) + Expect(id).To(Equal(alarmDefinitionID)) + }) + + It("should fail when alarm definition is not found in cache but there was a previous resync due to a missing dictionary ID or node cluster type ID", func() { + nodeClusterTypeID := uuid.New() + alarmDictionaryID := uuid.New() + alarmDefinitionID := uuid.New() + alarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm1", + Severity: "critical", + } + + clusterServer.nodeClusterTypeIDToAlarmDictionaryID = make(map[uuid.UUID]uuid.UUID) + clusterServer.nodeClusterTypeIDToAlarmDictionaryID[nodeClusterTypeID] = alarmDictionaryID + clusterServer.alarmDictionaryIDToAlarmDefinitions = make(map[uuid.UUID]AlarmDefinition) + + alarmDictionary := commonsgenerated.AlarmDictionary{ + AlarmDictionaryId: alarmDictionaryID, + AlarmDefinition: []commonsgenerated.AlarmDefinition{ + { + AlarmDefinitionId: alarmDefinitionID, + AlarmName: alarmDefinitionIdentifier.Name, + AlarmAdditionalFields: &map[string]interface{}{ + "severity": alarmDefinitionIdentifier.Severity, + }, + }, + }, + } + body, err := json.Marshal(alarmDictionary) + Expect(err).To(BeNil()) + + mockRepo.EXPECT().GetAlarmDictionary(gomock.Any(), alarmDictionaryID).Return( + &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader(body)), + }, nil) + + missingAlarmDefinitionIdentifier := AlarmDefinitionUniqueIdentifier{ + Name: "alarm2", + Severity: "warning", + } + + _, err = clusterServer.GetAlarmDefinitionID(nodeClusterTypeID, missingAlarmDefinitionIdentifier.Name, missingAlarmDefinitionIdentifier.Severity) + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/internal/service/alarms/internal/infrastructure/infrastructure.go b/internal/service/alarms/internal/infrastructure/infrastructure.go index 2cb26241..14a40202 100644 --- a/internal/service/alarms/internal/infrastructure/infrastructure.go +++ b/internal/service/alarms/internal/infrastructure/infrastructure.go @@ -3,15 +3,26 @@ package infrastructure import ( "context" "fmt" + "time" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure/clusterserver" + "github.com/google/uuid" + + "github.com/openshift-kni/oran-o2ims/internal/service/common/api/generated" ) +const resyncInterval = 1 * time.Hour + // Client is the interface that wraps the basic methods for the infrastructure clients type Client interface { Name() string Setup() error + FetchAll(context.Context) error + + GetObjectTypeID(objectID uuid.UUID) (uuid.UUID, error) + GetAlarmDefinitionID(ObjectTypeID uuid.UUID, name, severity string) (uuid.UUID, error) + + ReSync(ctx context.Context) } // Infrastructure represents the infrastructure clients @@ -19,10 +30,18 @@ type Infrastructure struct { Clients []Client } +type AlarmDictionary = generated.AlarmDictionary + +type AlarmDefinition map[AlarmDefinitionUniqueIdentifier]uuid.UUID +type AlarmDefinitionUniqueIdentifier struct { + Name string + Severity string +} + // Init sets up the infrastructure clients and fetches all the data func Init(ctx context.Context) (*Infrastructure, error) { // Currently only the cluster server is supported - clients := []Client{&clusterserver.ClusterServer{}} + clients := []Client{&ClusterServer{}} for _, server := range clients { if err := server.Setup(); err != nil { @@ -30,8 +49,10 @@ func Init(ctx context.Context) (*Infrastructure, error) { } if err := server.FetchAll(ctx); err != nil { - return nil, fmt.Errorf("failed to fetch objects from %s: %w", server.Name(), err) + return nil, fmt.Errorf("failed to fetch all data for %s: %w", server.Name(), err) } + + server.ReSync(ctx) } return &Infrastructure{Clients: clients}, nil diff --git a/internal/service/alarms/internal/infrastructure/suite_test.go b/internal/service/alarms/internal/infrastructure/suite_test.go new file mode 100644 index 00000000..484346c3 --- /dev/null +++ b/internal/service/alarms/internal/infrastructure/suite_test.go @@ -0,0 +1,13 @@ +package infrastructure + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestRepo(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Alarms Infrastructure Suite") +} diff --git a/internal/service/alarms/serve.go b/internal/service/alarms/serve.go index 44bc715b..e3c5c853 100644 --- a/internal/service/alarms/serve.go +++ b/internal/service/alarms/serve.go @@ -24,7 +24,6 @@ import ( "github.com/openshift-kni/oran-o2ims/internal/service/alarms/api/generated" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/alertmanager" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/db/repo" - "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/dictionary_collector" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/infrastructure" "github.com/openshift-kni/oran-o2ims/internal/service/alarms/internal/notifier_provider" common "github.com/openshift-kni/oran-o2ims/internal/service/common/api" @@ -101,15 +100,6 @@ func Serve(config *api.AlarmsServerConfig) error { Db: pool, } - // Load dictionary - alarmDictionaryCollector, err := dictionary_collector.New(alarmRepository, infrastructureClients) - if err != nil { - return fmt.Errorf("error creating alarm dictionary collector: %w", err) - } - - // Run dictionary collector - alarmDictionaryCollector.Run(ctx) - // Parse global cloud id var globalCloudID uuid.UUID if config.GlobalCloudID != utils.DefaultOCloudID { diff --git a/internal/service/cluster/collector/collector_alarms.go b/internal/service/cluster/collector/collector_alarms.go index c963a1c1..e18c4327 100644 --- a/internal/service/cluster/collector/collector_alarms.go +++ b/internal/service/cluster/collector/collector_alarms.go @@ -348,6 +348,9 @@ func (d *AlarmsDataSource) createAlarmDefinitions(rules []monitoringv1.Rule, ala additionalFields["KeepFiringFor"] = string(*rule.KeepFiringFor) } + // Add severity to additional fields + additionalFields[utils.AlarmDefinitionSeverityField] = rule.Labels["severity"] + //TODO: Add info from prometheus rules containing the rule such as the namespace summary := rule.Annotations["summary"]