Fix icon upload for multi-team software titles (#41785)
Fixes #41688 --------- Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
Fixed custom app icons not getting set via GitOps when the same software title exists in multiple teams.
|
||||
+10
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user