diff --git a/changes/21082-fix-available-for-install-filter-for-host-software b/changes/21082-fix-available-for-install-filter-for-host-software new file mode 100644 index 00000000000..9c1b850570d --- /dev/null +++ b/changes/21082-fix-available-for-install-filter-for-host-software @@ -0,0 +1 @@ +* Fixed the "Available for install" filter in the host's software page so that installers that were requested to be installed on the host (regardless of installation status) also show up in the list. diff --git a/server/datastore/mysql/software.go b/server/datastore/mysql/software.go index 76ab0da294c..e9589469802 100644 --- a/server/datastore/mysql/software.go +++ b/server/datastore/mysql/software.go @@ -2101,6 +2101,21 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id ` } + var softwareIsInstalledOnHostClause string + if !opts.OnlyAvailableForInstall { + softwareIsInstalledOnHostClause = ` + EXISTS ( + SELECT 1 + FROM + host_software hs + INNER JOIN + software s ON hs.software_id = s.id + WHERE + hs.host_id = :host_id AND + s.title_id = st.id + ) OR ` + } + // this statement lists only the software that is reported as installed on // the host or has been attempted to be installed on the host. stmtInstalled := fmt.Sprintf(` @@ -2133,7 +2148,7 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id LEFT OUTER JOIN nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid WHERE - -- use the latest install only + -- use the latest install attempt only ( hsi.id IS NULL OR hsi.id = ( SELECT hsi2.id FROM host_software_installs hsi2 @@ -2146,22 +2161,15 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id WHERE hvsi2.host_id = hvsi.host_id AND hvsi2.adam_id = hvsi.adam_id AND hvsi2.platform = hvsi.platform ORDER BY hvsi2.created_at DESC LIMIT 1 ) ) AND - -- software is installed on host - ( EXISTS ( - SELECT 1 - FROM - host_software hs - INNER JOIN - software s ON hs.software_id = s.id - WHERE - hs.host_id = :host_id AND - s.title_id = st.id - ) OR - -- or software install has been attempted on host (via installer or VPP app) - hsi.host_id IS NOT NULL OR hvsi.host_id IS NOT NULL ) + + -- software is installed on host or software install has been attempted + -- on host (via installer or VPP app). If only available for install is + -- requested, then the software installed on host clause is empty. + ( %s hsi.host_id IS NOT NULL OR hvsi.host_id IS NOT NULL ) %s %s -`, softwareInstallerHostStatusNamedQuery("hsi", ""), vppAppHostStatusNamedQuery("hvsi", "ncr", ""), onlySelfServiceClause, onlyVulnerableClause) +`, softwareInstallerHostStatusNamedQuery("hsi", ""), vppAppHostStatusNamedQuery("hvsi", "ncr", ""), + softwareIsInstalledOnHostClause, onlySelfServiceClause, onlyVulnerableClause) // this statement lists only the software that has never been installed nor // attempted to be installed on the host, but that is available to be @@ -2262,20 +2270,14 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id } stmt := stmtInstalled - if opts.AvailableForInstall || (opts.IncludeAvailableForInstall && !opts.VulnerableOnly) { + if opts.OnlyAvailableForInstall || (opts.IncludeAvailableForInstall && !opts.VulnerableOnly) { namedArgs["vpp_apps_platforms"] = []fleet.AppleDevicePlatform{fleet.IOSPlatform, fleet.IPadOSPlatform, fleet.MacOSPlatform} if fleet.IsLinux(host.Platform) { namedArgs["host_compatible_platforms"] = fleet.HostLinuxOSs } else { namedArgs["host_compatible_platforms"] = []string{host.FleetPlatform()} } - if opts.AvailableForInstall { - // Only available for install software - stmt = stmtAvailable - } else { - // All software, including available for install - stmt += ` UNION ` + stmtAvailable - } + stmt += ` UNION ` + stmtAvailable } // must resolve the named bindings here, before adding the searchLike which diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go index b069975f9c9..46d3c59d9a7 100644 --- a/server/datastore/mysql/software_test.go +++ b/server/datastore/mysql/software_test.go @@ -3185,14 +3185,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { require.Equal(t, &fleet.PaginationMetadata{}, meta) // available for install only works too - opts.AvailableForInstall = true + opts.OnlyAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) assert.Empty(t, sw) assert.Equal(t, &fleet.PaginationMetadata{}, meta) // self-service only works too - opts.AvailableForInstall = false + opts.OnlyAvailableForInstall = false opts.SelfServiceOnly = true opts.IncludeAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) @@ -3386,12 +3386,12 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { opts.VulnerableOnly = false // No software that is available for install - opts.AvailableForInstall = true + opts.OnlyAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) assert.Empty(t, sw) assert.Equal(t, &fleet.PaginationMetadata{}, meta) - opts.AvailableForInstall = false + opts.OnlyAvailableForInstall = false // create some Fleet installers and map them to a software title, // including one for a team @@ -3582,15 +3582,18 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { require.Equal(t, &fleet.PaginationMetadata{TotalResults: 8}, meta) compareResults(expected, sw, true, i3.Name+i3.Source) - // request with available software only + // request with available software only (attempted to install and never attempted to install) expectedAvailableOnly := map[string]fleet.HostSoftwareWithInstaller{} + expectedAvailableOnly[byNSV[b].Name+byNSV[b].Source] = expected[byNSV[b].Name+byNSV[b].Source] + expectedAvailableOnly[i0.Name+i0.Source] = i0 + expectedAvailableOnly[i1.Name+i1.Source] = i1 expectedAvailableOnly[i2.Name+i2.Source] = i2 - opts.AvailableForInstall = true + opts.OnlyAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) - assert.Equal(t, &fleet.PaginationMetadata{TotalResults: 1}, meta) + assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly))}, meta) compareResults(expectedAvailableOnly, sw, true) - opts.AvailableForInstall = false + opts.OnlyAvailableForInstall = false // request in descending order opts.ListOptions.OrderDirection = fleet.OrderDescending @@ -3639,6 +3642,8 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { Status: expectStatus(fleet.SoftwareInstallerPending), SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid4"}}, } + expectedAvailableOnly[byNSV[b].Name+byNSV[b].Source] = expected[byNSV[b].Name+byNSV[b].Source] + expectedAvailableOnly[i1.Name+i1.Source] = expected[i1.Name+i1.Source] // request without available software opts.IncludeAvailableForInstall = false @@ -3770,6 +3775,8 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { Status: nil, AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp3}, } + expectedAvailableOnly["vpp1apps"] = expected["vpp1apps"] + expectedAvailableOnly["vpp2apps"] = expected["vpp2apps"] expectedAvailableOnly["vpp3apps"] = expected["vpp3apps"] opts.IncludeAvailableForInstall = true opts.ListOptions.PerPage = 20 @@ -3779,12 +3786,12 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { compareResults(expected, sw, true, i3.Name+i3.Source) // i3 is for team // Available for install only - opts.AvailableForInstall = true + opts.OnlyAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) - assert.Equal(t, &fleet.PaginationMetadata{TotalResults: 2}, meta) + assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly))}, meta) compareResults(expectedAvailableOnly, sw, true) - opts.AvailableForInstall = false + opts.OnlyAvailableForInstall = false // team host sees available i3 and pending vpp1 opts.IncludeAvailableForInstall = true @@ -3881,14 +3888,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) { wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 2}, }, { - opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 0, PerPage: 2}, AvailableForInstall: true}, - wantNames: []string{"i2", "vpp3"}, - wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 2}, + opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 0, PerPage: 4}, OnlyAvailableForInstall: true}, + wantNames: []string{byNSV[b].Name, "i0", "i1", "i2"}, + wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 7}, }, { - opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 1}, AvailableForInstall: true}, - wantNames: []string{"vpp3"}, - wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 2}, + opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 4}, OnlyAvailableForInstall: true}, + wantNames: []string{"vpp1", "vpp2", "vpp3"}, + wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 7}, }, } for _, c := range cases { @@ -4091,12 +4098,12 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) { opts.VulnerableOnly = false // No software that is available for install - opts.AvailableForInstall = true + opts.OnlyAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) assert.Empty(t, sw) assert.Equal(t, &fleet.PaginationMetadata{}, meta) - opts.AvailableForInstall = false + opts.OnlyAvailableForInstall = false // Create a team tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "mobile team"}) @@ -4179,6 +4186,8 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) { AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp4}, } expectedAvailableOnly := map[string]fleet.HostSoftwareWithInstaller{} + expectedAvailableOnly["vpp1ios_apps"] = expected["vpp1ios_apps"] + expectedAvailableOnly["vpp2ios_apps"] = expected["vpp2ios_apps"] expectedAvailableOnly["vpp3ios_apps"] = expected["vpp3ios_apps"] expectedAvailableOnly["vpp4ios_apps"] = expected["vpp4ios_apps"] opts.IncludeAvailableForInstall = true @@ -4189,12 +4198,12 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) { compareResults(expected, sw, true) // Available for install only - opts.AvailableForInstall = true + opts.OnlyAvailableForInstall = true sw, meta, err = ds.ListHostSoftware(ctx, host, opts) require.NoError(t, err) assert.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expectedAvailableOnly))}, meta) compareResults(expectedAvailableOnly, sw, true) - opts.AvailableForInstall = false + opts.OnlyAvailableForInstall = false } diff --git a/server/fleet/software.go b/server/fleet/software.go index efc004ffc57..f03b5a4d36c 100644 --- a/server/fleet/software.go +++ b/server/fleet/software.go @@ -240,9 +240,10 @@ type HostSoftwareTitleListOptions struct { // install (but not currently installed on the host) should be returned. IncludeAvailableForInstall bool - // AvailableForInstall is a query argument that limits the returned software - // titles to those that are available for install on the host. - AvailableForInstall bool `query:"available_for_install,optional"` + // OnlyAvailableForInstall is set via a query argument that limits the + // returned software titles to only those that are available for install on + // the host. + OnlyAvailableForInstall bool `query:"available_for_install,optional"` VulnerableOnly bool `query:"vulnerable,optional"` }