From 744dece08cef991e750f9f81720a316d67d85ef5 Mon Sep 17 00:00:00 2001 From: Marcelo Guerrero Viveros Date: Fri, 31 Jan 2025 20:17:32 +0100 Subject: [PATCH] Fetch alarm dictionaries from cluster server (#531) The alarm server uses the alarm dictionary endpoints of the cluster server to fetch the alarm dictionaries. These dictionaries are cached in the same way as the nodeClusters and nodeClusterTypes objects. There are 2 new methods in the infrastructure client to return the nodeClusterTypeID and the alarmDefinitionID. These methods return the IDs if they are present in cache or try to fetch them from the cluster server if they are missing. There is also a resync mechanism that runs every hour. Signed-off-by: Marcelo Guerrero --- .../oran-o2ims.clusterserviceversion.yaml | 17 + config/rbac/role.yaml | 17 + internal/controllers/inventory_controller.go | 15 + internal/controllers/utils/constants.go | 3 + internal/service/alarms/api/server.go | 69 +- .../internal/alertmanager/alertmanager.go | 42 +- ...001_create_alarm_dictionary_table.down.sql | 8 - ...00001_create_alarm_dictionary_table.up.sql | 31 - ..._create_alarm_event_record_table.down.sql} | 0 ...01_create_alarm_event_record_table.up.sql} | 0 ...002_create_alarm_definition_table.down.sql | 14 - ...00002_create_alarm_definition_table.up.sql | 65 -- ...te_alarm_subscription_info_table.down.sql} | 0 ...eate_alarm_subscription_info_table.up.sql} | 0 ...larm_service_configuration_table.down.sql} | 0 ..._alarm_service_configuration_table.up.sql} | 0 .../internal/db/models/alarm_definition.go | 45 - .../internal/db/models/alarm_dictionary.go | 38 - .../internal/db/repo/alarms_repository.go | 161 +--- .../db/repo/alarms_repository_interface.go | 6 - .../db/repo/alarms_repository_test.go | 227 ----- .../db/repo/generated/mock_repo.generated.go | 89 -- .../internal/dictionary_collector/cluster.go | 452 ---------- .../dictionary_collector/collector.go | 83 -- .../dictionary_collector_test.go | 223 ----- .../dictionary_collector/suite_test.go | 13 - .../internal/infrastructure/clusterserver.go | 439 ++++++++++ .../clusterserver/clusterserver.go | 193 ---- .../clusterserver/generated/generate.go | 1 + .../mock_generated/mock_client.gen.go | 824 ++++++++++++++++++ .../infrastructure/clusterserver_test.go | 475 ++++++++++ .../internal/infrastructure/infrastructure.go | 27 +- .../internal/infrastructure/suite_test.go | 13 + internal/service/alarms/serve.go | 10 - .../cluster/collector/collector_alarms.go | 3 + 35 files changed, 1871 insertions(+), 1732 deletions(-) delete mode 100644 internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.down.sql delete mode 100644 internal/service/alarms/internal/db/migrations/000001_create_alarm_dictionary_table.up.sql rename internal/service/alarms/internal/db/migrations/{000003_create_alarm_event_record_table.down.sql => 000001_create_alarm_event_record_table.down.sql} (100%) rename internal/service/alarms/internal/db/migrations/{000003_create_alarm_event_record_table.up.sql => 000001_create_alarm_event_record_table.up.sql} (100%) delete mode 100644 internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.down.sql delete mode 100644 internal/service/alarms/internal/db/migrations/000002_create_alarm_definition_table.up.sql rename internal/service/alarms/internal/db/migrations/{000004_create_alarm_subscription_info_table.down.sql => 000002_create_alarm_subscription_info_table.down.sql} (100%) rename internal/service/alarms/internal/db/migrations/{000004_create_alarm_subscription_info_table.up.sql => 000002_create_alarm_subscription_info_table.up.sql} (100%) rename internal/service/alarms/internal/db/migrations/{000005_create_alarm_service_configuration_table.down.sql => 000003_create_alarm_service_configuration_table.down.sql} (100%) rename internal/service/alarms/internal/db/migrations/{000005_create_alarm_service_configuration_table.up.sql => 000003_create_alarm_service_configuration_table.up.sql} (100%) delete mode 100644 internal/service/alarms/internal/db/models/alarm_definition.go delete mode 100644 internal/service/alarms/internal/db/models/alarm_dictionary.go delete mode 100644 internal/service/alarms/internal/dictionary_collector/cluster.go delete mode 100644 internal/service/alarms/internal/dictionary_collector/collector.go delete mode 100644 internal/service/alarms/internal/dictionary_collector/dictionary_collector_test.go delete mode 100644 internal/service/alarms/internal/dictionary_collector/suite_test.go create mode 100644 internal/service/alarms/internal/infrastructure/clusterserver.go delete mode 100644 internal/service/alarms/internal/infrastructure/clusterserver/clusterserver.go create mode 100644 internal/service/alarms/internal/infrastructure/clusterserver/generated/mock_generated/mock_client.gen.go create mode 100644 internal/service/alarms/internal/infrastructure/clusterserver_test.go create mode 100644 internal/service/alarms/internal/infrastructure/suite_test.go 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"]