Fix icon upload for multi-team software titles (#41785)

Fixes #41688

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
Carlo
2026-03-17 06:55:22 -04:00
committed by GitHub
co-authored by Ian Littman
parent 616578a27c
commit c0fc3787fc
4 changed files with 99 additions and 3 deletions
+1
View File
@@ -0,0 +1 @@
Fixed custom app icons not getting set via GitOps when the same software title exists in multiple teams.
@@ -245,7 +245,7 @@ const EditIconModal = ({
});
};
const { data: customIconData } = useQuery(
const { data: customIconData, isError: isCustomIconError } = useQuery(
["softwareIcon", softwareId, teamIdForApi, iconUploadedAt],
() => softwareAPI.getSoftwareIcon(softwareId, teamIdForApi),
{
@@ -336,6 +336,13 @@ const EditIconModal = ({
// useQuery does not handle dimension extraction, so this is required for updating
// state with image details after loading the icon blob in the browser
useEffect(() => {
// If the icon fetch failed, stop showing the spinner and fall back
if (isCustomIconError && isFirstLoadWithCustomIcon) {
setIsFirstLoadWithCustomIcon(false);
resetIconState();
return;
}
// Handle API custom icon blob conversion and initialization
if (
shouldFetchCustomIcon &&
@@ -375,6 +382,8 @@ const EditIconModal = ({
}
}, [
customIconData,
isCustomIconError,
isFirstLoadWithCustomIcon,
iconState.status,
shouldFetchCustomIcon,
iconState.previewUrl,
@@ -161,9 +161,13 @@ func (ds *Datastore) ActivityDetailsForSoftwareTitleIcon(ctx context.Context, te
INNER JOIN software_titles ON software_title_icons.software_title_id = software_titles.id
LEFT JOIN teams ON software_title_icons.team_id = teams.id
LEFT JOIN software_installers ON software_installers.title_id = software_titles.id
AND software_installers.global_or_team_id = software_title_icons.team_id
LEFT JOIN in_house_apps ON in_house_apps.title_id = software_titles.id
AND in_house_apps.global_or_team_id = software_title_icons.team_id
LEFT JOIN vpp_apps ON vpp_apps.title_id = software_titles.id
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.adam_id = vpp_apps.adam_id AND vpp_apps_teams.platform = vpp_apps.platform
LEFT JOIN vpp_apps_teams ON vpp_apps_teams.adam_id = vpp_apps.adam_id
AND vpp_apps_teams.platform = vpp_apps.platform
AND vpp_apps_teams.global_or_team_id = software_title_icons.team_id
WHERE software_title_icons.team_id = ? AND software_title_icons.software_title_id = ?
`
err := sqlx.GetContext(ctx, ds.reader(ctx), &details, query, teamID, titleID)
@@ -256,7 +256,7 @@ func testDeleteSoftwareTitleIcon(t *testing.T, ds *Datastore) {
func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) {
ctx := context.Background()
var teamID, titleID, installerID uint
var teamID, titleID, installerID, installer2ID uint
var err error
testCases := []struct {
name string
@@ -481,6 +481,88 @@ func testActivityDetailsForSoftwareTitleIcon(t *testing.T, ds *Datastore) {
require.Nil(t, activity.LabelsExcludeAny)
require.Nil(t, activity.LabelsIncludeAny)
}},
{"multi-team software installer", func(ds *Datastore) {
// Create two teams sharing the same software title with installers in both teams.
// This verifies that the query returns exactly one row per team instead of N rows.
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
teamID, titleID, err = createTeamAndSoftwareTitle(t, ctx, ds)
require.NoError(t, err)
// Create a second team
tm2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team2"})
require.NoError(t, err)
// Create installer in team 1
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
installerID, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "hello",
InstallerFile: tfr1,
StorageID: "storage1",
Filename: "foo.pkg",
Title: "foo",
Version: "0.0.3",
Source: "apps",
TeamID: &teamID,
UserID: user.ID,
BundleIdentifier: "foo.bundle.id",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// Create installer in team 2 for the same title
tfr2, err := fleet.NewTempFileReader(strings.NewReader("world"), t.TempDir)
require.NoError(t, err)
installer2ID, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "world",
InstallerFile: tfr2,
StorageID: "storage2",
Filename: "foo.pkg",
Title: "foo",
Version: "0.0.3",
Source: "apps",
TeamID: &tm2.ID,
UserID: user.ID,
BundleIdentifier: "foo.bundle.id",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// Create icons in both teams
_, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{
TeamID: teamID,
TitleID: titleID,
StorageID: "icon-storage-1",
Filename: "icon1.png",
})
require.NoError(t, err)
_, err = ds.CreateOrUpdateSoftwareTitleIcon(ctx, &fleet.UploadSoftwareTitleIconPayload{
TeamID: tm2.ID,
TitleID: titleID,
StorageID: "icon-storage-2",
Filename: "icon2.png",
})
require.NoError(t, err)
}, func(t *testing.T, ds *Datastore) {
// Query for team 1 should return exactly one row with team 1's installer
activity1, err := ds.ActivityDetailsForSoftwareTitleIcon(ctx, teamID, titleID)
require.NoError(t, err)
require.Equal(t, installerID, *activity1.SoftwareInstallerID)
require.Equal(t, "team1", *activity1.TeamName)
require.Equal(t, teamID, activity1.TeamID)
// Query for team 2 should also succeed (not produce multiple rows)
tm2, err := ds.TeamByName(ctx, "team2")
require.NoError(t, err)
require.NotZero(t, tm2.ID)
activity2, err := ds.ActivityDetailsForSoftwareTitleIcon(ctx, tm2.ID, titleID)
require.NoError(t, err)
require.NotNil(t, activity2.SoftwareInstallerID)
require.Equal(t, installer2ID, *activity2.SoftwareInstallerID)
require.Equal(t, "team2", *activity2.TeamName)
require.Equal(t, tm2.ID, activity2.TeamID)
}},
{"in house app", func(ds *Datastore) {
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
teamID, titleID, err = createTeamAndSoftwareTitle(t, ctx, ds)