From 8d1a2a583f309976b1135645ce59d629ac390693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Dugovi=C4=8D?= Date: Fri, 11 Sep 2026 11:49:06 +0200 Subject: [PATCH 1/2] RHINENG-29747: add workspace_id not null constraint --- .../167_add_workspace_id_not_null.down.sql | 43 +++++++++++++++++++ .../167_add_workspace_id_not_null.up.sql | 42 ++++++++++++++++++ database_admin/schema/create_schema.sql | 5 +-- 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 database_admin/migrations/167_add_workspace_id_not_null.down.sql create mode 100644 database_admin/migrations/167_add_workspace_id_not_null.up.sql diff --git a/database_admin/migrations/167_add_workspace_id_not_null.down.sql b/database_admin/migrations/167_add_workspace_id_not_null.down.sql new file mode 100644 index 000000000..106281852 --- /dev/null +++ b/database_admin/migrations/167_add_workspace_id_not_null.down.sql @@ -0,0 +1,43 @@ +ALTER TABLE system_inventory ALTER COLUMN workspace_id DROP NOT NULL; + +CREATE OR REPLACE FUNCTION refresh_account_advisory_caches_multi(advisory_ids_in INTEGER[] DEFAULT NULL, + rh_account_id_in INTEGER DEFAULT NULL) + RETURNS VOID AS +$refresh_account_advisory$ +BEGIN + PERFORM aa.rh_account_id, aa.workspace_id, aa.advisory_id + FROM account_advisory aa + WHERE (aa.advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) + AND (aa.rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL) + FOR UPDATE OF aa; + + WITH current_counts AS ( + SELECT sa.advisory_id, sa.rh_account_id, si.workspace_id, + count(sa.*) FILTER (WHERE sa.status_id = 0) AS systems_installable, + count(sa.*) AS systems_applicable + FROM system_advisories sa + JOIN system_inventory si + ON sa.rh_account_id = si.rh_account_id AND sa.system_id = si.id + JOIN system_patch sp + ON si.id = sp.system_id AND sp.rh_account_id = si.rh_account_id + WHERE sp.last_evaluation IS NOT NULL + AND si.stale = FALSE + AND si.workspace_id IS NOT NULL + AND (sa.advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) + AND (si.rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL) + GROUP BY sa.advisory_id, sa.rh_account_id, si.workspace_id + ), + upserted AS ( + INSERT INTO account_advisory (advisory_id, rh_account_id, workspace_id, systems_installable, systems_applicable) + SELECT advisory_id, rh_account_id, workspace_id, systems_installable, systems_applicable + FROM current_counts + ON CONFLICT (rh_account_id, workspace_id, advisory_id) DO UPDATE SET + systems_installable = EXCLUDED.systems_installable, + systems_applicable = EXCLUDED.systems_applicable + ) + DELETE FROM account_advisory + WHERE (advisory_id, rh_account_id, workspace_id) NOT IN (SELECT advisory_id, rh_account_id, workspace_id FROM current_counts) + AND (advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) + AND (rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL); +END; +$refresh_account_advisory$ LANGUAGE plpgsql; diff --git a/database_admin/migrations/167_add_workspace_id_not_null.up.sql b/database_admin/migrations/167_add_workspace_id_not_null.up.sql new file mode 100644 index 000000000..3b14fa58c --- /dev/null +++ b/database_admin/migrations/167_add_workspace_id_not_null.up.sql @@ -0,0 +1,42 @@ +ALTER TABLE system_inventory ALTER COLUMN workspace_id SET NOT NULL; + +CREATE OR REPLACE FUNCTION refresh_account_advisory_caches_multi(advisory_ids_in INTEGER[] DEFAULT NULL, + rh_account_id_in INTEGER DEFAULT NULL) + RETURNS VOID AS +$refresh_account_advisory$ +BEGIN + PERFORM aa.rh_account_id, aa.workspace_id, aa.advisory_id + FROM account_advisory aa + WHERE (aa.advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) + AND (aa.rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL) + FOR UPDATE OF aa; + + WITH current_counts AS ( + SELECT sa.advisory_id, sa.rh_account_id, si.workspace_id, + count(sa.*) FILTER (WHERE sa.status_id = 0) AS systems_installable, + count(sa.*) AS systems_applicable + FROM system_advisories sa + JOIN system_inventory si + ON sa.rh_account_id = si.rh_account_id AND sa.system_id = si.id + JOIN system_patch sp + ON si.id = sp.system_id AND sp.rh_account_id = si.rh_account_id + WHERE sp.last_evaluation IS NOT NULL + AND si.stale = FALSE + AND (sa.advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) + AND (si.rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL) + GROUP BY sa.advisory_id, sa.rh_account_id, si.workspace_id + ), + upserted AS ( + INSERT INTO account_advisory (advisory_id, rh_account_id, workspace_id, systems_installable, systems_applicable) + SELECT advisory_id, rh_account_id, workspace_id, systems_installable, systems_applicable + FROM current_counts + ON CONFLICT (rh_account_id, workspace_id, advisory_id) DO UPDATE SET + systems_installable = EXCLUDED.systems_installable, + systems_applicable = EXCLUDED.systems_applicable + ) + DELETE FROM account_advisory + WHERE (advisory_id, rh_account_id, workspace_id) NOT IN (SELECT advisory_id, rh_account_id, workspace_id FROM current_counts) + AND (advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) + AND (rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL); +END; +$refresh_account_advisory$ LANGUAGE plpgsql; diff --git a/database_admin/schema/create_schema.sql b/database_admin/schema/create_schema.sql index 9a543cb2c..8486330f0 100644 --- a/database_admin/schema/create_schema.sql +++ b/database_admin/schema/create_schema.sql @@ -7,7 +7,7 @@ CREATE TABLE IF NOT EXISTS schema_migrations INSERT INTO schema_migrations -VALUES (166, false); +VALUES (167, false); -- --------------------------------------------------------------------------- -- Functions @@ -192,7 +192,6 @@ BEGIN ON si.id = sp.system_id AND sp.rh_account_id = si.rh_account_id WHERE sp.last_evaluation IS NOT NULL AND si.stale = FALSE - AND si.workspace_id IS NOT NULL AND (sa.advisory_id = ANY (advisory_ids_in) OR advisory_ids_in IS NULL) AND (si.rh_account_id = rh_account_id_in OR rh_account_id_in IS NULL) GROUP BY sa.advisory_id, sa.rh_account_id, si.workspace_id @@ -654,7 +653,7 @@ CREATE TABLE IF NOT EXISTS system_inventory ansible_workload_controller_version TEXT CHECK (NOT empty(ansible_workload_controller_version)), mssql_workload BOOLEAN NOT NULL DEFAULT false, mssql_workload_version TEXT CHECK (NOT empty(mssql_workload_version)), - workspace_id UUID, + workspace_id UUID NOT NULL, workspace_name TEXT CHECK (NOT empty(workspace_name)), crowdstrike_workload BOOLEAN NOT NULL DEFAULT false, ibm_db2_workload BOOLEAN NOT NULL DEFAULT false, From bb2e084716aeacc135df48b271b2ead2104ad51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Dugovi=C4=8D?= Date: Fri, 11 Sep 2026 12:06:30 +0200 Subject: [PATCH 2/2] RHINENG-29747: remove workspaceID nil logic WorkspaceID will not be nil after the migration in previous commit. Co-Authored-By: Gemini --- base/database/testing.go | 5 +-- base/models/models.go | 8 ++-- evaluator/advisory_update.go | 24 +++--------- evaluator/advisory_update_test.go | 14 +++++-- listener/common_test.go | 5 +-- listener/upload.go | 37 ++++++++++--------- listener/upload_test.go | 5 +-- manager/controllers/common_attributes.go | 6 +-- .../template_systems_update_test.go | 2 +- tasks/system_culling/system_culling_test.go | 2 +- turnpike/controllers/admin_test.go | 2 +- 11 files changed, 51 insertions(+), 59 deletions(-) diff --git a/base/database/testing.go b/base/database/testing.go index 95d602989..95cb34117 100644 --- a/base/database/testing.go +++ b/base/database/testing.go @@ -18,9 +18,8 @@ const ( TestWorkspace1ID = "aaaaaaaa-0000-0000-0000-000000000001" ) -func TestWorkspace1IDPtr() *uuid.UUID { - id := uuid.MustParse(TestWorkspace1ID) - return &id +func TestWorkspace1UUID() uuid.UUID { + return uuid.MustParse(TestWorkspace1ID) } func TestWorkspace1NamePtr() *string { diff --git a/base/models/models.go b/base/models/models.go index d8f87e514..273f14274 100644 --- a/base/models/models.go +++ b/base/models/models.go @@ -83,10 +83,10 @@ type SystemInventory struct { BuiltPkgcache bool `gorm:"column:built_pkgcache"` Arch *string Bootc bool - Tags []byte `gorm:"column:tags"` - Created time.Time // set by trigger system_platform_insert_trigger - WorkspaceID *uuid.UUID `gorm:"column:workspace_id"` - WorkspaceName *string `gorm:"column:workspace_name"` + Tags []byte `gorm:"column:tags"` + Created time.Time // set by trigger system_platform_insert_trigger + WorkspaceID uuid.UUID `gorm:"column:workspace_id"` + WorkspaceName *string `gorm:"column:workspace_name"` StaleTimestamp *time.Time StaleWarningTimestamp *time.Time CulledTimestamp *time.Time diff --git a/evaluator/advisory_update.go b/evaluator/advisory_update.go index 635b0618d..4b2b82e05 100644 --- a/evaluator/advisory_update.go +++ b/evaluator/advisory_update.go @@ -8,7 +8,6 @@ import ( "app/base/utils" "time" - "github.com/google/uuid" "github.com/pkg/errors" ) @@ -30,22 +29,6 @@ func getChangedAdvisoryIDs(advisoriesByName extendedAdvisoryMap) []int64 { return ids } -func createAdvisoryUpdateEvent(system *models.SystemPlatformV2, advisoryIDs []int64) mqueue.AdvisoryUpdateEvent { - var workspaceID uuid.UUID - if system.Inventory.WorkspaceID != nil { - workspaceID = *system.Inventory.WorkspaceID - } else { - utils.LogWarn("inventoryID", system.GetInventoryID(), "no workspace for system") - } - - return mqueue.AdvisoryUpdateEvent{ - RhAccountID: system.Inventory.RhAccountID, - WorkspaceID: workspaceID, - AdvisoryIDs: advisoryIDs, - ProducedAt: types.Rfc3339Timestamp(time.Now()), - } -} - func publishAdvisoryUpdates(system *models.SystemPlatformV2, advisoriesByName extendedAdvisoryMap) error { if advisoryUpdatePublisher == nil { return nil @@ -63,7 +46,12 @@ func publishAdvisoryUpdates(system *models.SystemPlatformV2, advisoriesByName ex return nil } - event := createAdvisoryUpdateEvent(system, advisoryIDs) + event := mqueue.AdvisoryUpdateEvent{ + RhAccountID: system.Inventory.RhAccountID, + WorkspaceID: system.Inventory.WorkspaceID, + AdvisoryIDs: advisoryIDs, + ProducedAt: types.Rfc3339Timestamp(time.Now()), + } if err := mqueue.SendMessages(base.Context, advisoryUpdatePublisher, &mqueue.AdvisoryUpdateEvents{event}); err != nil { return errors.Wrap(err, "writing advisory update events") } diff --git a/evaluator/advisory_update_test.go b/evaluator/advisory_update_test.go index 270cf6966..948a9e8f9 100644 --- a/evaluator/advisory_update_test.go +++ b/evaluator/advisory_update_test.go @@ -5,8 +5,10 @@ import ( "app/base/database" "app/base/models" "app/base/mqueue" + "app/base/types" "app/base/utils" "testing" + "time" "github.com/bytedance/sonic" "github.com/google/uuid" @@ -34,15 +36,19 @@ func TestCreateAdvisoryUpdateEvent(t *testing.T) { ID: 1, RhAccountID: rhAccountID, InventoryID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), - WorkspaceID: &wsID, + WorkspaceID: wsID, WorkspaceName: &wsName, }, Patch: models.SystemPatch{}, } changedAdvisoryIDs := []int64{1, 2} - - event := createAdvisoryUpdateEvent(system, changedAdvisoryIDs) + event := mqueue.AdvisoryUpdateEvent{ + RhAccountID: system.Inventory.RhAccountID, + WorkspaceID: system.Inventory.WorkspaceID, + AdvisoryIDs: changedAdvisoryIDs, + ProducedAt: types.Rfc3339Timestamp(time.Now()), + } assert.Equal(t, rhAccountID, event.RhAccountID) assert.Equal(t, wsID, event.WorkspaceID) assert.ElementsMatch(t, changedAdvisoryIDs, event.AdvisoryIDs) @@ -60,7 +66,7 @@ func TestPublishAdvisoryUpdates(t *testing.T) { ID: 1, RhAccountID: rhAccountID, InventoryID: uuid.MustParse("00000000-0000-0000-0000-000000000001"), - WorkspaceID: &wsID, + WorkspaceID: wsID, WorkspaceName: &wsName, }, Patch: models.SystemPatch{}, diff --git a/listener/common_test.go b/listener/common_test.go index c4606bf23..d84773850 100644 --- a/listener/common_test.go +++ b/listener/common_test.go @@ -120,14 +120,13 @@ func assertSystemInventoryProfileMatchesHost(t *testing.T, inventoryID uuid.UUID assert.JSONEq(t, string(utils.MarshalNilToJSONB(host.Tags)), string(inv.Tags)) if len(host.Groups) == 0 { - assert.Nil(t, inv.WorkspaceID) + assert.Equal(t, uuid.Nil, inv.WorkspaceID) assert.Nil(t, inv.WorkspaceName) } else { if hostWorkspaceID := host.Groups[0].ID; hostWorkspaceID != "" { - require.NotNil(t, inv.WorkspaceID) assert.Equal(t, hostWorkspaceID, inv.WorkspaceID.String()) } else { - require.Nil(t, inv.WorkspaceID) + assert.Equal(t, uuid.Nil, inv.WorkspaceID) } if hostWorkspaceName := host.Groups[0].Name; hostWorkspaceName != "" { require.NotNil(t, inv.WorkspaceName) diff --git a/listener/upload.go b/listener/upload.go index a626b3e6f..d66b8afff 100644 --- a/listener/upload.go +++ b/listener/upload.go @@ -364,25 +364,26 @@ func updateSystemPlatform(tx *gorm.DB, accountID int, host *Host, isBootc := len(host.SystemProfile.BootcStatus.Booted.Image) > 0 updatesReqJSONString := string(updatesReqJSON) - var workspaceID *uuid.UUID var workspaceName *string - if l := len(host.Groups); l > 0 { - workspace := host.Groups[0] - uuid, err := uuid.Parse(workspace.ID) - if err != nil { - utils.LogError("workspaceID", workspace.ID, "invalid workspace UUID") - return nil, errors.New("received invalid workspace UUID") - } - workspaceID = &uuid - if workspace.Name != "" { - workspaceName = &workspace.Name - } - if l != 1 { - utils.LogWarn( - "host_id", host.ID, "org_id", host.OrgID, "workspaces", host.Groups, - "received a host with multiple workspaces", - ) - } + l := len(host.Groups) + if l == 0 { + utils.LogError("inventoryID", inventoryID, "workspace UUID missing for system") + return nil, errors.New("workspace UUID missing for system") + } + workspace := host.Groups[0] + workspaceID, err := uuid.Parse(workspace.ID) + if err != nil { + utils.LogError("workspaceID", workspace.ID, "invalid workspace UUID") + return nil, errors.New("received invalid workspace UUID") + } + if workspace.Name != "" { + workspaceName = &workspace.Name + } + if l != 1 { + utils.LogWarn( + "host_id", host.ID, "org_id", host.OrgID, "workspaces", host.Groups, + "received a host with multiple workspaces", + ) } systemPlatform := &models.SystemPlatformV2{ Inventory: models.SystemInventory{ diff --git a/listener/upload_test.go b/listener/upload_test.go index b701c8f78..add771d5d 100644 --- a/listener/upload_test.go +++ b/listener/upload_test.go @@ -490,7 +490,7 @@ func TestStoreOrUpdateSysPlatform(t *testing.T) { SatelliteManaged: false, Created: hostEvent.Host.Created, Tags: utils.MarshalNilToJSONB(hostEvent.Host.Tags), - WorkspaceID: &workspaceID, + WorkspaceID: workspaceID, WorkspaceName: workspaceName, OSName: utils.EmptyToNil(&hostEvent.Host.SystemProfile.OperatingSystem.Name), OSMajor: &hostEvent.Host.SystemProfile.OperatingSystem.Major, @@ -535,7 +535,6 @@ func TestStoreOrUpdateSysPlatform(t *testing.T) { assert.Contains(t, string(inventoryAfterInsert.Tags), `"key": "env"`) assert.Contains(t, string(inventoryAfterInsert.Tags), `"value": "prod"`) - require.NotNil(t, inventoryAfterInsert.WorkspaceID) assert.Equal(t, hostEvent.Host.Groups[0].ID, inventoryAfterInsert.WorkspaceID.String()) require.NotNil(t, inventoryAfterInsert.WorkspaceName) assert.Equal(t, hostEvent.Host.Groups[0].Name, *inventoryAfterInsert.WorkspaceName) @@ -578,7 +577,7 @@ func TestStoreOrUpdateSysPlatform(t *testing.T) { SatelliteManaged: true, Created: hostEvent.Host.Created, Tags: utils.MarshalNilToJSONB(hostEvent.Host.Tags), - WorkspaceID: &workspaceID, + WorkspaceID: workspaceID, WorkspaceName: workspaceName, OSName: utils.EmptyToNil(&hostEvent.Host.SystemProfile.OperatingSystem.Name), OSMajor: &hostEvent.Host.SystemProfile.OperatingSystem.Major, diff --git a/manager/controllers/common_attributes.go b/manager/controllers/common_attributes.go index bb99144d1..e05cf285c 100644 --- a/manager/controllers/common_attributes.go +++ b/manager/controllers/common_attributes.go @@ -29,12 +29,12 @@ type SystemTags struct { } type SystemGroups struct { - Groups SystemGroupsList `json:"groups" csv:"groups" query:"CASE WHEN si.workspace_id IS NOT NULL THEN jsonb_build_array(jsonb_build_object('id', si.workspace_id, 'name', si.workspace_name)) ELSE '[]'::jsonb END" order_query:"si.workspace_name"` + Groups SystemGroupsList `json:"groups" csv:"groups" query:"jsonb_build_array(jsonb_build_object('id', si.workspace_id, 'name', si.workspace_name))" order_query:"si.workspace_name"` } type SystemWorkspace struct { - WorkspaceID *uuid.UUID `json:"workspace_id" csv:"workspace_id" query:"si.workspace_id" gorm:"column:workspace_id"` - WorkspaceName *string `json:"workspace_name" csv:"workspace_name" query:"si.workspace_name" gorm:"column:workspace_name"` + WorkspaceID uuid.UUID `json:"workspace_id" csv:"workspace_id" query:"si.workspace_id" gorm:"column:workspace_id"` + WorkspaceName *string `json:"workspace_name" csv:"workspace_name" query:"si.workspace_name" gorm:"column:workspace_name"` } // baseline attributes are obsoleted and we keep them only for backward API compatibility diff --git a/manager/controllers/template_systems_update_test.go b/manager/controllers/template_systems_update_test.go index 5b09ff53c..0d2951415 100644 --- a/manager/controllers/template_systems_update_test.go +++ b/manager/controllers/template_systems_update_test.go @@ -179,7 +179,7 @@ func testUpdateTemplateBadRequest(t *testing.T, satelliteManaged, bootc bool) { RhAccountID: templateAccount, DisplayName: "template_bad_request_test", Tags: []byte("[]"), - WorkspaceID: database.TestWorkspace1IDPtr(), + WorkspaceID: database.TestWorkspace1UUID(), WorkspaceName: database.TestWorkspace1NamePtr(), BuiltPkgcache: true, SatelliteManaged: satelliteManaged, diff --git a/tasks/system_culling/system_culling_test.go b/tasks/system_culling/system_culling_test.go index a8f318af5..7bef40137 100644 --- a/tasks/system_culling/system_culling_test.go +++ b/tasks/system_culling/system_culling_test.go @@ -177,7 +177,7 @@ func TestCullSystems(t *testing.T) { RhAccountID: 1, DisplayName: invID, Tags: []byte("[]"), - WorkspaceID: database.TestWorkspace1IDPtr(), + WorkspaceID: database.TestWorkspace1UUID(), WorkspaceName: database.TestWorkspace1NamePtr(), CulledTimestamp: &staleDate, } diff --git a/turnpike/controllers/admin_test.go b/turnpike/controllers/admin_test.go index 5a48ce7cd..1f1ed9637 100644 --- a/turnpike/controllers/admin_test.go +++ b/turnpike/controllers/admin_test.go @@ -24,7 +24,7 @@ func TestInitDelete(t *testing.T) { RhAccountID: 1, DisplayName: del, Tags: []byte("[]"), - WorkspaceID: database.TestWorkspace1IDPtr(), + WorkspaceID: database.TestWorkspace1UUID(), WorkspaceName: database.TestWorkspace1NamePtr(), } assert.NoError(t, database.DB.Create(&inv).Error)