Exclude failed installs from hosts All Software list (#38266)
Fixes #29046.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user