Exclude failed installs from hosts All Software list (#38266)

Fixes #29046.
This commit is contained in:
Carlo
2026-01-14 11:18:22 -05:00
committed by GitHub
parent 418df6ba09
commit c1e0460a76
2 changed files with 187 additions and 0 deletions
+34
View File
@@ -5051,6 +5051,40 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opt
}
}
// Filter out-of-scope FAILED installs from all app types.
// Only remove if not in inventory AND status is failed AND out of label scope.
for titleID, st := range bySoftwareTitleID {
if st.InstallerID != nil {
// Check if software is NOT actually installed (not in osquery inventory)
if _, isInstalled := hostInstalledSoftwareTitleSet[titleID]; !isInstalled {
// Only remove if install FAILED and installer is out of scope
if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed {
if _, inScope := filteredBySoftwareTitleID[titleID]; !inScope {
delete(bySoftwareTitleID, titleID)
}
}
}
}
}
for adamID, st := range byVPPAdamID {
if _, isInstalled := hostInstalledSoftwareTitleSet[st.ID]; !isInstalled {
if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed {
if _, inScope := filteredByVPPAdamID[adamID]; !inScope {
delete(byVPPAdamID, adamID)
}
}
}
}
for appID, st := range byInHouseID {
if _, isInstalled := hostInstalledSoftwareTitleSet[st.ID]; !isInstalled {
if st.Status != nil && *st.Status == fleet.SoftwareInstallFailed {
if _, inScope := filteredByInHouseID[appID]; !inScope {
delete(byInHouseID, appID)
}
}
}
}
if opts.OnlyAvailableForInstall {
bySoftwareTitleID = filteredBySoftwareTitleID
byVPPAdamID = filteredByVPPAdamID
+153
View File
@@ -87,6 +87,8 @@ func TestSoftware(t *testing.T) {
{"CreateIntermediateInstallFailureRecord", testCreateIntermediateInstallFailureRecord},
{"ListHostSoftwareInstallThenTransferTeam", testListHostSoftwareInstallThenTransferTeam},
{"ListHostSoftwareFailInstallThenTransferTeam", testListHostSoftwareFailInstallThenTransferTeam},
{"ListHostSoftwareFailInstallThenLabelExclude", testListHostSoftwareFailInstallThenLabelExclude},
{"ListHostSoftwareFailVPPInstallThenLabelExclude", testListHostSoftwareFailVPPInstallThenLabelExclude},
{"ListHostSoftwareInstallThenDeleteInstallers", testListHostSoftwareInstallThenDeleteInstallers},
{"ListSoftwareVersionsVulnerabilityFilters", testListSoftwareVersionsVulnerabilityFilters},
{"TestListHostSoftwareWithLabelScoping", testListHostSoftwareWithLabelScoping},
@@ -6378,6 +6380,157 @@ func testListHostSoftwareFailInstallThenTransferTeam(t *testing.T, ds *Datastore
require.NotNil(t, sw[0].SoftwarePackage)
}
// testListHostSoftwareFailInstallThenLabelExclude tests that when a host has a failed
// install attempt and then the installer's label targeting is changed to exclude the host,
// the software no longer appears in the host's "All software" list (IncludeAvailableForInstall=true).
func testListHostSoftwareFailInstallThenLabelExclude(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
// Use IncludeAvailableForInstall (not OnlyAvailableForInstall) - this is the "All software" view
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: false, // Explicitly false - we want "All software" view
}
team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team1.ID, []uint{host.ID}))
require.NoError(t, err)
host.TeamID = &team1.ID
// Create a label that we'll use to exclude the host
excludeLabel, err := ds.NewLabel(ctx, &fleet.Label{
Name: "exclude-label",
Query: "select 1",
})
require.NoError(t, err)
// Create a software installer with NO label targeting (available to all hosts in team)
tfr, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
require.NoError(t, err)
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "exit 1", // Will fail
InstallerFile: tfr,
StorageID: "storage1",
Filename: "bar.pkg",
Title: "bar",
Version: "1.0",
Source: "apps",
Platform: string(fleet.MacOSPlatform),
TeamID: &team1.ID,
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{}, // No labels initially
}
installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, payload)
require.NoError(t, err)
// Fail to install the software on the host
hostInstall, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: hostInstall,
InstallScriptExitCode: ptr.Int(1), // Failed
}, nil)
require.NoError(t, err)
// Verify software appears in "All software" view (failed install should show)
sw, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Equal(t, "bar", sw[0].Name)
require.NotNil(t, sw[0].SoftwarePackage)
// Now add the host to the exclude label
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{excludeLabel.ID}))
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
// Add the exclude label to the installer's targeting
// This simulates changing the installer's targeting to exclude hosts with this label
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx,
`INSERT INTO software_installer_labels (software_installer_id, label_id, exclude) VALUES (?, ?, 1)`,
installerID, excludeLabel.ID)
return err
})
// Now the software should NOT appear in "All software" view because:
// 1. The software is not installed (failed install)
// 2. The installer is out of scope (host is in the exclude label)
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 0, "Software with failed install should not appear when installer targeting excludes the host")
}
func testListHostSoftwareFailVPPInstallThenLabelExclude(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)
host := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now(), test.WithPlatform("darwin"))
nanoEnroll(t, ds, host, false)
opts := fleet.HostSoftwareTitleListOptions{
ListOptions: fleet.ListOptions{PerPage: 10, IncludeMetadata: true, OrderKey: "name", TestSecondaryOrderKey: "source"},
IncludeAvailableForInstall: true,
OnlyAvailableForInstall: false,
IsMDMEnrolled: true,
}
dataToken, err := test.CreateVPPTokenData(time.Now().Add(24*time.Hour), "Test org"+t.Name(), "Test location"+t.Name())
require.NoError(t, err)
tok1, err := ds.InsertVPPToken(ctx, dataToken)
require.NoError(t, err)
_, err = ds.UpdateVPPTokenTeams(ctx, tok1.ID, []uint{})
require.NoError(t, err)
vppApp := &fleet.VPPApp{Name: "vpp_app_fail_test", VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "999", Platform: fleet.MacOSPlatform}}, BundleIdentifier: "com.test.vppfail"}
vppApp, err = ds.InsertVPPAppWithTeam(ctx, vppApp, nil)
require.NoError(t, err)
vppAppTeamID := vppApp.VPPAppTeam.AppTeamID
excludeLabel, err := ds.NewLabel(ctx, &fleet.Label{Name: "exclude-vpp-label", Query: "select 1"})
require.NoError(t, err)
// Create a FAILED VPP install request
vppCmdUUID := createVPPAppInstallRequest(t, ds, host, vppApp.AdamID, user)
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host.ID, "")
require.NoError(t, err)
createVPPAppInstallResult(t, ds, host, vppCmdUUID, fleet.MDMAppleStatusError)
// Verify VPP app appears in "All software" view (failed install should show initially)
sw, _, err := ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 1)
require.Equal(t, "vpp_app_fail_test", sw[0].Name)
// Add host to exclude label
require.NoError(t, ds.AddLabelsToHost(ctx, host.ID, []uint{excludeLabel.ID}))
host.LabelUpdatedAt = time.Now()
err = ds.UpdateHost(ctx, host)
require.NoError(t, err)
time.Sleep(time.Second)
// Add exclude label to VPP app targeting
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx,
`INSERT INTO vpp_app_team_labels (vpp_app_team_id, label_id, exclude) VALUES (?, ?, 1)`,
vppAppTeamID, excludeLabel.ID)
return err
})
// Now, the VPP app should NOT appear because it failed and is out of scope
sw, _, err = ds.ListHostSoftware(ctx, host, opts)
require.NoError(t, err)
require.Len(t, sw, 0, "VPP app with failed install should not appear when targeting excludes the host")
}
func testListHostSoftwareInstallThenDeleteInstallers(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "user1", "user1@example.com", false)