Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
48 changes: 25 additions & 23 deletions server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
51 changes: 30 additions & 21 deletions server/datastore/mysql/software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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"})
Expand Down Expand Up @@ -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
Expand All @@ -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

}

Expand Down
7 changes: 4 additions & 3 deletions server/fleet/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Expand Down