From c79540d5ccb2ad088321b8848c500122976033ce Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Thu, 8 Jan 2026 10:54:18 +0100 Subject: [PATCH 1/7] RHINENG-22325: configure topic for Inventory Views --- base/utils/config.go | 9 +++++++++ conf/common.env | 1 + conf/local.env | 3 ++- deploy/clowdapp.yaml | 5 +++++ dev/kafka/setup.sh | 16 +++++++++++----- 5 files changed, 28 insertions(+), 6 deletions(-) diff --git a/base/utils/config.go b/base/utils/config.go index 432700ca3..13e8f7c07 100644 --- a/base/utils/config.go +++ b/base/utils/config.go @@ -66,6 +66,7 @@ type coreConfig struct { RemediationUpdateTopic string NotificationsTopic string TemplateTopic string + InventoryViewsTopic string // services VmaasAddress string @@ -149,6 +150,11 @@ func initDBFromEnv() { } func initKafkaFromEnv() { + overrideKafkaAddress := Getenv("KAFKA_ADDRESS", "") + if overrideKafkaAddress != "" { + CoreCfg.KafkaAddress = overrideKafkaAddress + CoreCfg.KafkaServers = []string{overrideKafkaAddress} + } CoreCfg.KafkaSslCert = Getenv("KAFKA_SSL_CERT", CoreCfg.KafkaSslCert) CoreCfg.KafkaSslSkipVerify = GetBoolEnvOrDefault("KAFKA_SSL_SKIP_VERIFY", false) CoreCfg.KafkaUsername = Getenv("KAFKA_USERNAME", CoreCfg.KafkaUsername) @@ -167,6 +173,7 @@ func initTopicsFromEnv() { CoreCfg.RemediationUpdateTopic = Getenv("REMEDIATIONS_UPDATE_TOPIC", "") CoreCfg.NotificationsTopic = Getenv("NOTIFICATIONS_TOPIC", "") CoreCfg.TemplateTopic = Getenv("TEMPLATE_TOPIC", "") + CoreCfg.InventoryViewsTopic = Getenv("INVENTORY_VIEWS_TOPIC", "") } func initServicesFromEnv() { @@ -248,6 +255,7 @@ func initKafkaFromClowder() { translateTopic(&CoreCfg.RemediationUpdateTopic) translateTopic(&CoreCfg.NotificationsTopic) translateTopic(&CoreCfg.TemplateTopic) + translateTopic(&CoreCfg.InventoryViewsTopic) } } @@ -399,6 +407,7 @@ func printKafkaParams() { fmt.Printf("REMEDIATIONS_UPDATE_TOPIC=%s\n", CoreCfg.RemediationUpdateTopic) fmt.Printf("NOTIFICATIONS_TOPIC=%s\n", CoreCfg.NotificationsTopic) fmt.Printf("TEMPLATE_TOPIC=%s\n", CoreCfg.TemplateTopic) + fmt.Printf("INVENTORY_VIEWS_TOPIC=%s\n", CoreCfg.InventoryViewsTopic) } func printServicesParams() { diff --git a/conf/common.env b/conf/common.env index c895e56b2..d1697f0a0 100644 --- a/conf/common.env +++ b/conf/common.env @@ -17,6 +17,7 @@ NOTIFICATIONS_TOPIC=platform.notifications.ingress PAYLOAD_TRACKER_TOPIC=platform.payload-status REMEDIATIONS_UPDATE_TOPIC=platform.remediation-updates.patch TEMPLATE_TOPIC=platform.content-sources.template +INVENTORY_VIEWS_TOPIC=platform.inventory.host-apps # If vmaas is running locally, its available here #VMAAS_ADDRESS=http://vmaas_webapp:8080 diff --git a/conf/local.env b/conf/local.env index 79138a088..b0251fd2a 100644 --- a/conf/local.env +++ b/conf/local.env @@ -16,13 +16,14 @@ DB_SSLROOTCERT=dev/database/secrets/pgca.crt VMAAS_ADDRESS=http://localhost:9001 CANDLEPIN_ADDRESS=http://localhost:9001/candlepin -#KAFKA_ADDRESS=localhost:29092 +KAFKA_ADDRESS=localhost:9092 KAFKA_GROUP=patchman KAFKA_SSL_CERT=dev/kafka/secrets/ca.crt PAYLOAD_TRACKER_TOPIC=platform.payload-status EVENTS_TOPIC=platform.inventory.events EVAL_TOPIC=patchman.evaluator.user-evaluation TEMPLATE_TOPIC=platform.content-sources.template +INVENTORY_VIEWS_TOPIC=platform.inventory.host-apps RBAC_ADDRESS=http://localhost:9001 diff --git a/deploy/clowdapp.yaml b/deploy/clowdapp.yaml index ab1855539..e807e39e7 100644 --- a/deploy/clowdapp.yaml +++ b/deploy/clowdapp.yaml @@ -205,6 +205,7 @@ objects: - {name: PAYLOAD_TRACKER_TOPIC, value: platform.payload-status} - {name: REMEDIATIONS_UPDATE_TOPIC, value: 'platform.remediation-updates.patch'} - {name: NOTIFICATIONS_TOPIC, value: 'platform.notifications.ingress'} + - {name: INVENTORY_VIEWS_TOPIC, value: '${INVENTORY_VIEWS_TOPIC}'} - {name: SSL_CERT_DIR, value: '${SSL_CERT_DIR}'} - {name: GOGC, value: '${GOGC}'} # set garbage collection limit for go 1.18 - {name: ENABLE_PROFILER, value: '${ENABLE_PROFILER_EVALUATOR_UPLOAD}'} @@ -251,6 +252,7 @@ objects: - {name: PAYLOAD_TRACKER_TOPIC, value: platform.payload-status} - {name: REMEDIATIONS_UPDATE_TOPIC, value: 'platform.remediation-updates.patch'} - {name: NOTIFICATIONS_TOPIC, value: 'platform.notifications.ingress'} + - {name: INVENTORY_VIEWS_TOPIC, value: '${INVENTORY_VIEWS_TOPIC}'} - {name: SSL_CERT_DIR, value: '${SSL_CERT_DIR}'} - {name: GOGC, value: '${GOGC}'} # set garbage collection limit for go 1.18 - {name: ENABLE_PROFILER, value: '${ENABLE_PROFILER_EVALUATOR_RECALC}'} @@ -297,6 +299,7 @@ objects: - {name: PAYLOAD_TRACKER_TOPIC, value: platform.payload-status} - {name: REMEDIATIONS_UPDATE_TOPIC, value: 'platform.remediation-updates.patch'} - {name: NOTIFICATIONS_TOPIC, value: 'platform.notifications.ingress'} + - {name: INVENTORY_VIEWS_TOPIC, value: '${INVENTORY_VIEWS_TOPIC}'} - {name: SSL_CERT_DIR, value: '${SSL_CERT_DIR}'} - {name: GOGC, value: '${GOGC}'} # set garbage collection limit for go 1.18 - {name: ENABLE_PROFILER, value: '${ENABLE_PROFILER_EVALUATOR_USER_EVALUATION}'} @@ -518,6 +521,7 @@ objects: kafkaTopics: - {replicas: 3, partitions: 10, topicName: platform.inventory.events} + - {replicas: 3, partitions: 10, topicName: platform.inventory.host-apps} - {replicas: 3, partitions: 10, topicName: patchman.evaluator.upload} - {replicas: 3, partitions: 10, topicName: patchman.evaluator.recalc} - {replicas: 3, partitions: 8, topicName: platform.payload-status} @@ -702,6 +706,7 @@ parameters: - {name: GOGC, value: '100'} - {name: GOMEMLIMIT_EVALUATOR, value: '2700MiB'} # set to 90% of the default memory limit (don't forget `B`) - {name: CONSOLEDOT_HOSTNAME, value: localhost} +- {name: INVENTORY_VIEWS_TOPIC, value: 'platform.inventory.host-apps'} # Evaluator - upload - {name: REPLICAS_EVALUATOR_UPLOAD, value: '1'} diff --git a/dev/kafka/setup.sh b/dev/kafka/setup.sh index 4c7b362b3..ce3fe38b2 100755 --- a/dev/kafka/setup.sh +++ b/dev/kafka/setup.sh @@ -4,11 +4,17 @@ sleep 5 # create topics with multiple partitions for scaling -for topic in "platform.inventory.events" "patchman.evaluator.upload" \ - "patchman.evaluator.recalc" "platform.remediation-updates.patch" "platform.notifications.ingress" \ - "platform.payload-status" "test" \ - "platform.content-sources.template" \ - "patchman.evaluator.user-evaluation" +for topic in \ + "patchman.evaluator.recalc" \ + "patchman.evaluator.upload" \ + "patchman.evaluator.user-evaluation" \ + "platform.content-sources.template" \ + "platform.inventory.events" \ + "platform.inventory.host-apps" \ + "platform.notifications.ingress" \ + "platform.payload-status" \ + "platform.remediation-updates.patch" \ + "test" do until /opt/kafka/bin/kafka-topics.sh --create --if-not-exists --topic $topic \ --partitions 1 --bootstrap-server kafka:9092 --replication-factor 1; do From d41428793923face22f927ec400a2ba0b1eb9a0b Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Thu, 8 Jan 2026 13:34:57 +0100 Subject: [PATCH 2/7] RHINENG-22325: structs for Inventory Views message --- base/inventory_views/inventory_views.go | 101 ++++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 base/inventory_views/inventory_views.go diff --git a/base/inventory_views/inventory_views.go b/base/inventory_views/inventory_views.go new file mode 100644 index 000000000..cc7583faf --- /dev/null +++ b/base/inventory_views/inventory_views.go @@ -0,0 +1,101 @@ +package inventory_views + +import ( + "app/base/models" + "app/base/utils" + + "gorm.io/gorm" +) + +type InventoryViewsHost struct { + // Inventory ID (UUID) of the host + ID string `json:"id"` + Data InventoryViewsHostData `json:"data"` +} + +type InventoryViewsHostData struct { + ApplicableRhsaCount int `json:"applicable_rhsa_count"` + ApplicableRhbaCount int `json:"applicable_rhba_count"` + ApplicableRheaCount int `json:"applicable_rhea_count"` + ApplicableOtherCount int `json:"applicable_other_count"` + InstallableRhsaCount int `json:"installable_rhsa_count"` + InstallableRhbaCount int `json:"installable_rhba_count"` + InstallableRheaCount int `json:"installable_rhea_count"` + InstallableOtherCount int `json:"installable_other_count"` + PackagesInstalled int `json:"packages_installed"` + PackagesInstallable int `json:"packages_installable"` + PackagesApplicable int `json:"packages_applicable"` + TemplateName *string `json:"template_name"` + TemplateUUID *string `json:"template_uuid"` +} + +type InventoryViewsEvent struct { + OrgID string `json:"org_id"` + Timestamp string `json:"timestamp"` + Hosts []InventoryViewsHost `json:"hosts"` +} + +func MakeInventoryViewsHosts(systems []models.SystemPlatform, + templates map[int64]models.Template) []InventoryViewsHost { + hosts := make([]InventoryViewsHost, len(systems)) + for i, system := range systems { + hosts[i] = InventoryViewsHost{ + ID: system.InventoryID, + Data: InventoryViewsHostData{ + ApplicableRhsaCount: system.ApplicableAdvisorySecCountCache, + ApplicableRhbaCount: system.ApplicableAdvisoryBugCountCache, + ApplicableRheaCount: system.ApplicableAdvisoryEnhCountCache, + ApplicableOtherCount: system.ApplicableAdvisoryCountCache - system.ApplicableAdvisorySecCountCache - + system.ApplicableAdvisoryBugCountCache - system.ApplicableAdvisoryEnhCountCache, + InstallableRhsaCount: system.InstallableAdvisorySecCountCache, + InstallableRhbaCount: system.InstallableAdvisoryBugCountCache, + InstallableRheaCount: system.InstallableAdvisoryEnhCountCache, + InstallableOtherCount: system.InstallableAdvisoryCountCache - system.InstallableAdvisorySecCountCache - + system.InstallableAdvisoryBugCountCache - system.InstallableAdvisoryEnhCountCache, + PackagesInstalled: system.PackagesInstalled, + PackagesInstallable: system.PackagesInstallable, + PackagesApplicable: system.PackagesApplicable, + }, + } + if system.TemplateID != nil { + template, ok := templates[*system.TemplateID] + if ok { + hosts[i].Data.TemplateName = &template.Name + hosts[i].Data.TemplateUUID = &template.UUID + } else { + utils.LogWarn("template_id", system.TemplateID, "template not found") + } + } + } + return hosts +} + +func FindSystemsTemplates(tx *gorm.DB, systems []models.SystemPlatform) (map[int64]models.Template, error) { + templateIDs := make([]int64, 0, len(systems)) + if len(systems) == 0 { + return nil, nil + } + for _, system := range systems { + if system.TemplateID == nil { + continue + } + templateIDs = append(templateIDs, *system.TemplateID) + } + + if len(templateIDs) == 0 { + return nil, nil + } + templates := make([]models.Template, 0, len(templateIDs)) + q := tx.Model(&models.Template{}). + Where("rh_account_id = ? AND id IN (?)", systems[0].RhAccountID, templateIDs) + err := q.Find(&templates).Error + if err != nil { + return nil, err + } + + templatesMap := make(map[int64]models.Template, len(templates)) + for _, t := range templates { + templatesMap[t.ID] = t + } + return templatesMap, nil +} From 71279a3ce83231a8c53a24afb7ba6e1deb21e615 Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Thu, 8 Jan 2026 13:49:13 +0100 Subject: [PATCH 3/7] RHINENG-22325: use only necessary subset of Template attributes for Inventory Views --- base/database/testing.go | 8 ++++++-- base/inventory_views/inventory_views.go | 10 +++++----- base/models/models.go | 14 +++++++++----- listener/templates.go | 8 +++++--- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/base/database/testing.go b/base/database/testing.go index bad34a398..2c7a3ae63 100644 --- a/base/database/testing.go +++ b/base/database/testing.go @@ -400,8 +400,12 @@ func DeleteSystem(t *testing.T, inventoryID string) { func CreateTemplate(t *testing.T, account int, uuid string, inventoryIDs []string) { template := &models.Template{ - RhAccountID: account, UUID: uuid, Name: uuid, EnvironmentID: strings.ReplaceAll(uuid, "-", ""), - Arch: "x86_64", Version: "8", + TemplateBase: models.TemplateBase{ + RhAccountID: account, UUID: uuid, Name: uuid, + }, + EnvironmentID: strings.ReplaceAll(uuid, "-", ""), + Arch: "x86_64", + Version: "8", } tx := DB.Begin() diff --git a/base/inventory_views/inventory_views.go b/base/inventory_views/inventory_views.go index cc7583faf..c8fb89bb2 100644 --- a/base/inventory_views/inventory_views.go +++ b/base/inventory_views/inventory_views.go @@ -36,7 +36,7 @@ type InventoryViewsEvent struct { } func MakeInventoryViewsHosts(systems []models.SystemPlatform, - templates map[int64]models.Template) []InventoryViewsHost { + templates map[int64]models.TemplateBase) []InventoryViewsHost { hosts := make([]InventoryViewsHost, len(systems)) for i, system := range systems { hosts[i] = InventoryViewsHost{ @@ -70,7 +70,7 @@ func MakeInventoryViewsHosts(systems []models.SystemPlatform, return hosts } -func FindSystemsTemplates(tx *gorm.DB, systems []models.SystemPlatform) (map[int64]models.Template, error) { +func FindSystemsTemplates(tx *gorm.DB, systems []models.SystemPlatform) (map[int64]models.TemplateBase, error) { templateIDs := make([]int64, 0, len(systems)) if len(systems) == 0 { return nil, nil @@ -85,15 +85,15 @@ func FindSystemsTemplates(tx *gorm.DB, systems []models.SystemPlatform) (map[int if len(templateIDs) == 0 { return nil, nil } - templates := make([]models.Template, 0, len(templateIDs)) - q := tx.Model(&models.Template{}). + templates := make([]models.TemplateBase, 0, len(templateIDs)) + q := tx.Model(&models.TemplateBase{}). Where("rh_account_id = ? AND id IN (?)", systems[0].RhAccountID, templateIDs) err := q.Find(&templates).Error if err != nil { return nil, err } - templatesMap := make(map[int64]models.Template, len(templates)) + templatesMap := make(map[int64]models.TemplateBase, len(templates)) for _, t := range templates { templatesMap[t.ID] = t } diff --git a/base/models/models.go b/base/models/models.go index 4dc352bff..a287026d9 100644 --- a/base/models/models.go +++ b/base/models/models.go @@ -25,12 +25,16 @@ func (Reporter) TableName() string { return "reporter" } +type TemplateBase struct { + ID int64 `gorm:"primaryKey"` + RhAccountID int `gorm:"primaryKey"` + UUID string + Name string +} + type Template struct { - ID int64 `gorm:"primaryKey"` - RhAccountID int `gorm:"primaryKey"` - UUID string + TemplateBase EnvironmentID string - Name string Arch string Version string // Config pgtype.JSONB // currently unused @@ -40,7 +44,7 @@ type Template struct { LastEdited *time.Time } -func (Template) TableName() string { +func (TemplateBase) TableName() string { return "template" } diff --git a/listener/templates.go b/listener/templates.go index c11411dde..e9c6fd0b3 100644 --- a/listener/templates.go +++ b/listener/templates.go @@ -118,10 +118,12 @@ func TemplateUpdate(template mqueue.TemplateResponse) error { } row := models.Template{ - RhAccountID: accountID, - UUID: template.UUID, + TemplateBase: models.TemplateBase{ + RhAccountID: accountID, + UUID: template.UUID, + Name: template.Name, + }, EnvironmentID: template.EnvironmentID, - Name: template.Name, Arch: template.Arch, Version: template.Version, //Config: nil, From 8f9bdc9a46e95979e20ed1d1222d70168b276c50 Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Thu, 8 Jan 2026 15:04:58 +0100 Subject: [PATCH 4/7] RHINENG-22325: extend KafkaMessage with Headers --- base/mqueue/message.go | 8 ++++++-- base/mqueue/mqueue.go | 6 ++++-- base/mqueue/mqueue_impl_gokafka.go | 4 ++-- evaluator/notifications.go | 2 +- evaluator/notifications_test.go | 2 +- evaluator/remediations.go | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/base/mqueue/message.go b/base/mqueue/message.go index 449334275..d0bfefb04 100644 --- a/base/mqueue/message.go +++ b/base/mqueue/message.go @@ -1,12 +1,16 @@ package mqueue -import "github.com/bytedance/sonic" +import ( + "github.com/bytedance/sonic" + "github.com/segmentio/kafka-go" +) -func MessageFromJSON(k string, v interface{}) (KafkaMessage, error) { +func MessageFromJSON(k string, v interface{}, h []kafka.Header) (KafkaMessage, error) { var m KafkaMessage var err error m.Key = []byte(k) + m.Headers = h m.Value, err = sonic.Marshal(v) return m, err } diff --git a/base/mqueue/mqueue.go b/base/mqueue/mqueue.go index 9d6877593..f2e9722f8 100644 --- a/base/mqueue/mqueue.go +++ b/base/mqueue/mqueue.go @@ -12,6 +12,7 @@ import ( "sync" "github.com/lestrrat-go/backoff/v2" + "github.com/segmentio/kafka-go" ) const errContextCanceled = "context canceled" @@ -42,8 +43,9 @@ func createLoggerFunc(counter Counter) func(fmt string, args ...interface{}) { } type KafkaMessage struct { - Key []byte - Value []byte + Key []byte + Value []byte + Headers []kafka.Header } type MessageHandler func(message KafkaMessage) error diff --git a/base/mqueue/mqueue_impl_gokafka.go b/base/mqueue/mqueue_impl_gokafka.go index 7b72b8c5c..d8bc576ac 100644 --- a/base/mqueue/mqueue_impl_gokafka.go +++ b/base/mqueue/mqueue_impl_gokafka.go @@ -39,7 +39,7 @@ func (t *kafkaGoReaderImpl) HandleMessages(handler MessageHandler) { } } // At this level, all errors are fatal - kafkaMessage := KafkaMessage{Key: m.Key, Value: m.Value} + kafkaMessage := KafkaMessage{Key: m.Key, Value: m.Value, Headers: m.Headers} if err = handler(kafkaMessage); err != nil { utils.LogPanic("err", err.Error(), "Handler failed") } @@ -61,7 +61,7 @@ type kafkaGoWriterImpl struct { func (t *kafkaGoWriterImpl) WriteMessages(ctx context.Context, msgs ...KafkaMessage) error { kafkaGoMessages := make([]kafka.Message, len(msgs)) for i, m := range msgs { - kafkaGoMessages[i] = kafka.Message{Key: m.Key, Value: m.Value} + kafkaGoMessages[i] = kafka.Message{Key: m.Key, Value: m.Value, Headers: m.Headers} } err := t.Writer.WriteMessages(ctx, kafkaGoMessages...) return err diff --git a/evaluator/notifications.go b/evaluator/notifications.go index bd5cb087f..5c66cc57c 100644 --- a/evaluator/notifications.go +++ b/evaluator/notifications.go @@ -120,7 +120,7 @@ func publishNewAdvisoriesNotification(tx *gorm.DB, system *models.SystemPlatform return errors.Wrap(err, "creating notification failed") } - msg, err := mqueue.MessageFromJSON(system.InventoryID, notif) + msg, err := mqueue.MessageFromJSON(system.InventoryID, notif, nil) if err != nil { return errors.Wrap(err, "creating message from notification failed") } diff --git a/evaluator/notifications_test.go b/evaluator/notifications_test.go index cfd46d31e..594d8b5ed 100644 --- a/evaluator/notifications_test.go +++ b/evaluator/notifications_test.go @@ -114,7 +114,7 @@ func TestAdvisoriesNotificationMessage(t *testing.T) { assert.Equal(t, displayName, notification.Context.DisplayName) assert.Equal(t, tags, notification.Context.Tags) - msg, err := mqueue.MessageFromJSON(inventoryID, notification) + msg, err := mqueue.MessageFromJSON(inventoryID, notification, nil) assert.Nil(t, err) assert.Equal(t, inventoryID, string(msg.Key)) diff --git a/evaluator/remediations.go b/evaluator/remediations.go index b223226b7..d2fbb8a90 100644 --- a/evaluator/remediations.go +++ b/evaluator/remediations.go @@ -87,7 +87,7 @@ func publishRemediationsState(system *models.SystemPlatform, response *vmaas.Upd } state := createRemediationsStateMsg(system.InventoryID, response) - msg, err := mqueue.MessageFromJSON(system.InventoryID, state) + msg, err := mqueue.MessageFromJSON(system.InventoryID, state, nil) if err != nil { return errors.Wrap(err, "formatting message") } From a9776c19c8bd8b7f402ca9748918711e8edd594e Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Thu, 8 Jan 2026 15:42:35 +0100 Subject: [PATCH 5/7] RHINENG-22325: sent inventory view event after evaluation --- base/inventory_views/inventory_views.go | 11 ++++ evaluator/evaluate.go | 13 +++++ evaluator/inventory_views.go | 70 +++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 evaluator/inventory_views.go diff --git a/base/inventory_views/inventory_views.go b/base/inventory_views/inventory_views.go index c8fb89bb2..6af81f920 100644 --- a/base/inventory_views/inventory_views.go +++ b/base/inventory_views/inventory_views.go @@ -3,6 +3,7 @@ package inventory_views import ( "app/base/models" "app/base/utils" + "time" "gorm.io/gorm" ) @@ -35,6 +36,16 @@ type InventoryViewsEvent struct { Hosts []InventoryViewsHost `json:"hosts"` } +func MakeInventoryViewsEvent(tx *gorm.DB, orgID string, systems []models.SystemPlatform) ( + InventoryViewsEvent, error) { + templates, err := FindSystemsTemplates(tx, systems) + if err != nil { + return InventoryViewsEvent{}, err + } + hosts := MakeInventoryViewsHosts(systems, templates) + return InventoryViewsEvent{OrgID: orgID, Timestamp: time.Now().Format(time.RFC3339), Hosts: hosts}, nil +} + func MakeInventoryViewsHosts(systems []models.SystemPlatform, templates map[int64]models.TemplateBase) []InventoryViewsHost { hosts := make([]InventoryViewsHost, len(systems)) diff --git a/evaluator/evaluate.go b/evaluator/evaluate.go index 66942fd23..9c36c7f49 100644 --- a/evaluator/evaluate.go +++ b/evaluator/evaluate.go @@ -59,6 +59,7 @@ var ( enableYumUpdatesEval bool nEvalGoroutines int enableInstantNotifications bool + enableInventoryViews bool enableSatelliteFunctionality bool errVmaasBadRequest = errors.New("vmaas bad request") ) @@ -79,6 +80,7 @@ func configure() { vmaasUpdatesURL = utils.FailIfEmpty(utils.CoreCfg.VmaasAddress, "VMAAS_ADDRESS") + base.VMaaSAPIPrefix + "/updates" configureRemediations() configureNotifications() + configureInventoryViews() configureStatus() } @@ -126,6 +128,8 @@ func confugureEvaluator() { nEvalGoroutines = utils.PodConfig.GetInt("max_goroutines", 1) // Send advisory notification immediately enableInstantNotifications = utils.PodConfig.GetBool("instant_notifications", true) + // Send inventory views events + enableInventoryViews = utils.PodConfig.GetBool("inventory_views", false) // Ignore templates for satellite managed systems enableSatelliteFunctionality = utils.PodConfig.GetBool("satellite_functionality", true) } @@ -481,6 +485,15 @@ func evaluateAndStore(system *models.SystemPlatform, return errors.Wrap(err, "Unable to update system") } + if enableInventoryViews { + err = publishInventoryViewsEvent(tx, []models.SystemPlatform{*system}, event) + if err != nil { + evaluationCnt.WithLabelValues("error-inventory-views-publish").Inc() + utils.LogError("orgID", event.GetOrgID(), "inventoryID", system.GetInventoryID(), "err", err.Error(), + "publishing inventory views event failed") + } + } + // Send instant notification with new advisories if enableInstantNotifications { err = publishNewAdvisoriesNotification(tx, system, event.GetOrgID(), systemAdvisoriesNew) diff --git a/evaluator/inventory_views.go b/evaluator/inventory_views.go new file mode 100644 index 000000000..75ded8916 --- /dev/null +++ b/evaluator/inventory_views.go @@ -0,0 +1,70 @@ +package evaluator + +import ( + "app/base" + "app/base/inventory_views" + "app/base/models" + "app/base/mqueue" + "app/base/utils" + "fmt" + "time" + + "github.com/pkg/errors" + "github.com/segmentio/kafka-go" + "gorm.io/gorm" +) + +var patchAppHeader = kafka.Header{Key: "application", Value: []byte("patch")} + +var inventoryViewsPublisher mqueue.Writer + +func configureInventoryViews() { + if topic := utils.CoreCfg.InventoryViewsTopic; topic != "" { + inventoryViewsPublisher = mqueue.NewKafkaWriterFromEnv(topic) + } +} + +func publishInventoryViewsEvent(tx *gorm.DB, systems []models.SystemPlatform, origin *mqueue.PlatformEvent) error { + if inventoryViewsPublisher == nil { + return nil + } + + tStart := time.Now() + defer utils.ObserveSecondsSince(tStart, evaluationPartDuration.WithLabelValues("inventory-views-publish")) + + orgID := origin.GetOrgID() + var requestID string + if len(origin.RequestIDs) > 0 { + requestID = origin.RequestIDs[0] + } else { + requestID = fmt.Sprintf("patch-%d", time.Now().UnixNano()) + } + + value, err := inventory_views.MakeInventoryViewsEvent(tx, orgID, systems) + if err != nil { + return errors.Wrap(err, "creating inventory views event failed") + } + + headers := []kafka.Header{patchAppHeader, {Key: "request_id", Value: []byte(requestID)}} + + msg, err := mqueue.MessageFromJSON(orgID, value, headers) + if err != nil { + return errors.Wrap(err, "creating inventory views message failed") + } + + err = inventoryViewsPublisher.WriteMessages(base.Context, msg) + if err != nil { + return errors.Wrap(err, "writing message to inventory views publisher failed") + } + + // log the event + systemIDs := make([]int64, len(systems)) + for i, s := range systems { + systemIDs[i] = s.ID + } + + utils.LogInfo("rh_account_ID", systems[0].RhAccountID, "systemIDs", systemIDs, + "inventory views event sent successfully") + + return nil +} From f63f3e0cff33364bc561fd2fc21b3d292806e79b Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Fri, 9 Jan 2026 15:39:34 +0100 Subject: [PATCH 6/7] RHINENG-22325: add missing org_id into evaluator tests --- evaluator/evaluate_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/evaluator/evaluate_test.go b/evaluator/evaluate_test.go index 979c21612..9054fc933 100644 --- a/evaluator/evaluate_test.go +++ b/evaluator/evaluate_test.go @@ -18,6 +18,7 @@ import ( var systemID = int64(12) var rhAccountID = 3 +var orgID = "org_3" func TestInit(_ *testing.T) { utils.TestLoadEnv("conf/evaluator_common.env", "conf/evaluator_upload.env") @@ -61,6 +62,7 @@ func TestEvaluate(t *testing.T) { err := evaluateHandler(mqueue.PlatformEvent{ SystemIDs: []string{"00000000-0000-0000-0000-000000000012", "00000000-0000-0000-0000-000000000011"}, RequestIDs: []string{"request-1", "request-2"}, + OrgID: &orgID, AccountID: rhAccountID}) assert.NoError(t, err) @@ -78,6 +80,7 @@ func TestEvaluate(t *testing.T) { err = evaluateHandler(mqueue.PlatformEvent{ SystemIDs: []string{"00000000-0000-0000-0000-000000000012"}, RequestIDs: []string{"request-1"}, + OrgID: &orgID, AccountID: rhAccountID}) assert.NoError(t, err) database.CheckSystemJustEvaluated(t, "00000000-0000-0000-0000-000000000012", 3, 1, 1, 0, @@ -122,6 +125,7 @@ func TestEvaluateYum(t *testing.T) { err := evaluateHandler(mqueue.PlatformEvent{ SystemIDs: []string{ID}, + OrgID: &orgID, AccountID: rhAccountID}) assert.NoError(t, err) From f5dd08b2e724c5bea5f8e22d6e927589885322e4 Mon Sep 17 00:00:00 2001 From: Michael Mraka Date: Thu, 8 Jan 2026 16:49:12 +0100 Subject: [PATCH 7/7] RHINENG-22325: inventory views tests --- base/inventory_views/inventory_views_test.go | 95 ++++++++++++++++++++ conf/test.env | 2 +- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 base/inventory_views/inventory_views_test.go diff --git a/base/inventory_views/inventory_views_test.go b/base/inventory_views/inventory_views_test.go new file mode 100644 index 000000000..a9068dc4a --- /dev/null +++ b/base/inventory_views/inventory_views_test.go @@ -0,0 +1,95 @@ +package inventory_views + +import ( + "app/base/core" + "app/base/database" + "app/base/models" + "app/base/utils" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +const rhAccountID = 1 + +func TestMakeInventoryViewsEvent(t *testing.T) { + utils.SkipWithoutDB(t) + core.SetupTestEnvironment() + tx := database.DB.Begin() + defer tx.Rollback() + + var rhAccount models.RhAccount + assert.NoError(t, tx.Where("id = ?", rhAccountID).First(&rhAccount).Error) + assert.NotEmpty(t, *rhAccount.OrgID) + + var systems []models.SystemPlatform + assert.NoError(t, tx.Where("rh_account_id = ? AND id in (1,3)", rhAccountID). + Order("id").Find(&systems).Error) + assert.Equal(t, 2, len(systems)) + + event, err := MakeInventoryViewsEvent(tx, *rhAccount.OrgID, systems) + assert.NoError(t, err) + assert.Equal(t, *rhAccount.OrgID, event.OrgID) + assert.NotEmpty(t, event.Timestamp) + _, err = time.Parse(time.RFC3339, event.Timestamp) + assert.NoError(t, err) + + // Verify hosts + assert.Equal(t, 2, len(event.Hosts)) + + assert.Equal(t, InventoryViewsHost{ + ID: "00000000-0000-0000-0000-000000000001", + Data: InventoryViewsHostData{2, 3, 3, 0, 2, 2, 1, 0, 0, 0, 0, + utils.PtrString("temp1-1"), utils.PtrString("99900000-0000-0000-0000-000000000001")}, + }, event.Hosts[0]) + + assert.Equal(t, InventoryViewsHost{ + ID: "00000000-0000-0000-0000-000000000003", + Data: InventoryViewsHostData{0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + utils.PtrString("temp2-1"), utils.PtrString("99900000-0000-0000-0000-000000000002")}, + }, event.Hosts[1]) +} + +func TestMakeInventoryViewsEventEmpty(t *testing.T) { + utils.SkipWithoutDB(t) + core.SetupTestEnvironment() + + tx := database.DB.Begin() + defer tx.Rollback() + + event, err := MakeInventoryViewsEvent(tx, "test-org", []models.SystemPlatform{}) + assert.NoError(t, err) + assert.Equal(t, "test-org", event.OrgID) + assert.Equal(t, 0, len(event.Hosts)) + assert.NotEmpty(t, event.Timestamp) +} + +func TestMakeInventoryViewsEventNoTemplate(t *testing.T) { + utils.SkipWithoutDB(t) + core.SetupTestEnvironment() + + tx := database.DB.Begin() + defer tx.Rollback() + + var rhAccount models.RhAccount + assert.NoError(t, tx.Where("id = ?", rhAccountID).First(&rhAccount).Error) + assert.NotEmpty(t, *rhAccount.OrgID) + orgID := *rhAccount.OrgID + + var systems []models.SystemPlatform + assert.NoError(t, tx.Where("rh_account_id = ? AND id in (4)", rhAccountID). + Order("id").Find(&systems).Error) + assert.Equal(t, 1, len(systems)) + + // Should not error, but template fields should be nil + event, err := MakeInventoryViewsEvent(tx, orgID, systems) + assert.NoError(t, err) + assert.Equal(t, 1, len(event.Hosts)) + + host := event.Hosts[0] + assert.Equal(t, "00000000-0000-0000-0000-000000000004", host.ID) + // Template fields should be nil when template is not found + assert.Nil(t, host.Data.TemplateName) + assert.Nil(t, host.Data.TemplateUUID) +} diff --git a/conf/test.env b/conf/test.env index 34cf64ddf..b1a21f66f 100644 --- a/conf/test.env +++ b/conf/test.env @@ -10,7 +10,7 @@ DB_PASSWD=passwd LIMIT_PAGE_SIZE=false # don't put "" or '' around the text otherwise they'll be included into content -POD_CONFIG=label=upload;vmaas_call_max_retries=100;template_change_eval=false;update_users;update_db_config;use_testing_db +POD_CONFIG=label=upload;vmaas_call_max_retries=100;template_change_eval=false;update_users;update_db_config;use_testing_db;inventory_views KESSEL_URL=platform:9005 KESSEL_INSECURE=true