Skip to content

Commit

Permalink
fix: move last install data into apps and packages (#20664)
Browse files Browse the repository at this point in the history
> Related issue: #20662

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
  • Loading branch information
jahzielv authored Jul 24, 2024
1 parent 3f5fd7f commit fda3785
Show file tree
Hide file tree
Showing 5 changed files with 88 additions and 49 deletions.
29 changes: 20 additions & 9 deletions server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -2287,15 +2287,6 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
titleIDs := make([]uint, 0, len(hostSoftwareList))
byTitleID := make(map[uint]*hostSoftware, len(hostSoftwareList))
for _, hs := range hostSoftwareList {
// promote the last install info to the proper destination fields
if hs.LastInstallInstallUUID != nil && *hs.LastInstallInstallUUID != "" {
hs.LastInstall = &fleet.HostSoftwareInstall{
InstallUUID: *hs.LastInstallInstallUUID,
}
if hs.LastInstallInstalledAt != nil {
hs.LastInstall.InstalledAt = *hs.LastInstallInstalledAt
}
}

// promote the package name and version to the proper destination fields
if hs.PackageName != nil {
Expand All @@ -2308,6 +2299,16 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
Version: version,
SelfService: hs.PackageSelfService,
}

// promote the last install info to the proper destination fields
if hs.LastInstallInstallUUID != nil && *hs.LastInstallInstallUUID != "" {
hs.SoftwarePackage.LastInstall = &fleet.HostSoftwareInstall{
InstallUUID: *hs.LastInstallInstallUUID,
}
if hs.LastInstallInstalledAt != nil {
hs.SoftwarePackage.LastInstall.InstalledAt = *hs.LastInstallInstalledAt
}
}
}

// promote the VPP app id and version to the proper destination fields
Expand All @@ -2322,6 +2323,16 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
SelfService: hs.VPPAppSelfService,
IconURL: hs.VPPAppIconURL,
}

// promote the last install info to the proper destination fields
if hs.LastInstallInstallUUID != nil && *hs.LastInstallInstallUUID != "" {
hs.AppStoreApp.LastInstall = &fleet.HostSoftwareInstall{
CommandUUID: *hs.LastInstallInstallUUID,
}
if hs.LastInstallInstalledAt != nil {
hs.AppStoreApp.LastInstall.InstalledAt = *hs.LastInstallInstalledAt
}
}
}

titleIDs = append(titleIDs, hs.ID)
Expand Down
62 changes: 37 additions & 25 deletions server/datastore/mysql/software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3296,8 +3296,33 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
require.True(t, ok)
require.Equal(t, e.Name, g.Name)
require.Equal(t, e.Source, g.Source)
require.Equal(t, e.SoftwarePackage, g.SoftwarePackage)
require.Equal(t, e.AppStoreApp, g.AppStoreApp)
if e.SoftwarePackage != nil {
require.Equal(t, e.SoftwarePackage.SelfService, g.SoftwarePackage.SelfService)
require.Equal(t, e.SoftwarePackage.IconURL, g.SoftwarePackage.IconURL)
require.Equal(t, e.SoftwarePackage.AppStoreID, g.SoftwarePackage.AppStoreID)
require.Equal(t, e.SoftwarePackage.Name, g.SoftwarePackage.Name)
require.Equal(t, e.SoftwarePackage.Version, g.SoftwarePackage.Version)
if e.SoftwarePackage.LastInstall != nil {
require.Equal(t, e.SoftwarePackage.LastInstall.CommandUUID, g.SoftwarePackage.LastInstall.CommandUUID)
require.Equal(t, e.SoftwarePackage.LastInstall.InstallUUID, g.SoftwarePackage.LastInstall.InstallUUID)
require.NotNil(t, g.SoftwarePackage.LastInstall.InstalledAt)
}
}

if e.AppStoreApp != nil {
require.Equal(t, e.AppStoreApp.SelfService, g.AppStoreApp.SelfService)
require.Equal(t, e.AppStoreApp.IconURL, g.AppStoreApp.IconURL)
require.Equal(t, e.AppStoreApp.AppStoreID, g.AppStoreApp.AppStoreID)
require.Equal(t, e.AppStoreApp.Name, g.AppStoreApp.Name)
require.Equal(t, e.AppStoreApp.Version, g.AppStoreApp.Version)
if e.AppStoreApp.LastInstall != nil {
require.Equal(t, e.AppStoreApp.LastInstall.InstallUUID, g.AppStoreApp.LastInstall.InstallUUID)
require.Equal(t, e.AppStoreApp.LastInstall.CommandUUID, g.AppStoreApp.LastInstall.CommandUUID)
require.NotNil(t, g.AppStoreApp.LastInstall.InstalledAt)
}
}
// require.Equal(t, e.SoftwarePackage, g.SoftwarePackage)
// require.Equal(t, e.AppStoreApp, g.AppStoreApp)
require.Len(t, g.InstalledVersions, len(e.InstalledVersions))
if len(e.InstalledVersions) > 0 {
byVers := make(map[string]fleet.HostSoftwareInstalledVersion, len(e.InstalledVersions))
Expand Down Expand Up @@ -3475,8 +3500,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "b",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerPending),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"}},
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
},
Expand All @@ -3485,17 +3509,15 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "i0",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerInstalled),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid2"},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-1.pkg", Version: "v1.0.0", SelfService: ptr.Bool(true)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-1.pkg", Version: "v1.0.0", SelfService: ptr.Bool(true), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid2"}},
}
expected[i0.Name+i0.Source] = i0

i1 := fleet.HostSoftwareWithInstaller{
Name: "i1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerFailed),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid3"},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid3"}},
}
expected[i1.Name+i1.Source] = i1

Expand All @@ -3511,7 +3533,6 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "i2",
Source: "apps",
Status: nil,
LastInstall: nil,
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", SelfService: ptr.Bool(false)},
}
expected[i2.Name+i2.Source] = i2
Expand All @@ -3520,7 +3541,6 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "i3",
Source: "apps",
Status: nil,
LastInstall: nil,
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-4.pkg", Version: "v4.0.0", SelfService: ptr.Bool(false)},
}
expected[i3.Name+i3.Source] = i3
Expand Down Expand Up @@ -3568,8 +3588,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "b",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerFailed),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"}},
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
},
Expand All @@ -3578,8 +3597,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "i1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerPending),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid4"},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid4"}},
}

// request without available software
Expand Down Expand Up @@ -3678,15 +3696,13 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "vpp1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerInstalled),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: vpp1CmdUUID},
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1},
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1CmdUUID}},
}
expected["vpp2apps"] = fleet.HostSoftwareWithInstaller{
Name: "vpp2",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerPending),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: vpp2bCmdUUID},
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2},
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2, LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp2bCmdUUID}},
}

opts.IncludeAvailableForInstall = false
Expand All @@ -3700,7 +3716,6 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "vpp3",
Source: "apps",
Status: nil,
LastInstall: nil,
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp3},
}
opts.IncludeAvailableForInstall = true
Expand All @@ -3721,8 +3736,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "vpp1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerPending),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: vpp1TmCmdUUID},
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1},
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1TmCmdUUID}},
},
}, sw, true)

Expand All @@ -3743,15 +3757,13 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
Name: "i1",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerPending),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI1UUID},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI1UUID}},
},
"i2apps": {
Name: "i2",
Source: "apps",
Status: expectStatus(fleet.SoftwareInstallerPending),
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI2UUID},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", SelfService: ptr.Bool(false)},
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI2UUID}},
},
}
compareResults(expectedOther, sw, true)
Expand Down
20 changes: 14 additions & 6 deletions server/fleet/software_installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,7 +311,6 @@ type HostSoftwareWithInstaller struct {
Name string `json:"name" db:"name"`
Source string `json:"source" db:"source"`
Status *SoftwareInstallerStatus `json:"status" db:"status"`
LastInstall *HostSoftwareInstall `json:"last_install"`
InstalledVersions []*HostSoftwareInstalledVersion `json:"installed_versions"`

// SoftwarePackage provides software installer package information, it is
Expand All @@ -331,16 +330,25 @@ type SoftwarePackageOrApp struct {
// Name is only present for software installer packages.
Name string `json:"name,omitempty"`

Version string `json:"version"`
SelfService *bool `json:"self_service,omitempty"`
IconURL *string `json:"icon_url"`
Version string `json:"version"`
SelfService *bool `json:"self_service,omitempty"`
IconURL *string `json:"icon_url"`
LastInstall *HostSoftwareInstall `json:"last_install"`
}

// HostSoftwareInstall represents installation of software on a host from a
// Fleet software installer.
type HostSoftwareInstall struct {
InstallUUID string `json:"install_uuid" db:"install_id"`
InstalledAt time.Time `json:"installed_at" db:"installed_at"`
// InstallUUID is the the UUID of the script execution issued to install the related software. This
// field is only used if the install we're describing was for an uploaded software installer.
// Empty if the install was for an App Store app.
InstallUUID string `json:"install_uuid,omitempty"`

// CommandUUID is the UUID of the MDM command issued to install the related software. This field
// is only used if the install we're describing was for an App Store app.
// Empty if the install was for an uploaded software installer.
CommandUUID string `json:"command_uuid,omitempty"`
InstalledAt time.Time `json:"installed_at"`
}

// HostSoftwareInstalledVersion represents a version of software installed on a
Expand Down
13 changes: 7 additions & 6 deletions server/service/integration_enterprise_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10061,10 +10061,11 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
getHostSoftwareResp := getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSoftwareResp)
require.Len(t, getHostSoftwareResp.Software, 1)
require.NotNil(t, getHostSoftwareResp.Software[0].LastInstall)
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage)
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall)
require.NotNil(t, getHostSoftwareResp.Software[0].Status)
require.Equal(t, fleet.SoftwareInstallerPending, *getHostSoftwareResp.Software[0].Status)
installUUID := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
installUUID := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID

gsirr := getSoftwareInstallResultsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
Expand All @@ -10085,7 +10086,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h2.ID, titleID), nil, http.StatusAccepted, &resp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h2.ID), nil, http.StatusOK, &getHostSoftwareResp)
require.Len(t, getHostSoftwareResp.Software, 1)
installUUID2 := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
installUUID2 := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
Expand All @@ -10097,7 +10098,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h3.ID, titleID), nil, http.StatusAccepted, &resp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h3.ID), nil, http.StatusOK, &getHostSoftwareResp)
require.Len(t, getHostSoftwareResp.Software, 1)
installUUID3 := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
installUUID3 := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
Expand All @@ -10109,7 +10110,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h4.ID, titleID), nil, http.StatusAccepted, &resp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h4.ID), nil, http.StatusOK, &getHostSoftwareResp)
require.Len(t, getHostSoftwareResp.Software, 1)
installUUID4a := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
installUUID4a := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
Expand All @@ -10119,7 +10120,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h4.ID, titleID), nil, http.StatusAccepted, &resp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h4.ID), nil, http.StatusOK, &getHostSoftwareResp)
require.Len(t, getHostSoftwareResp.Software, 1)
installUUID4b := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
installUUID4b := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
_ = installUUID4b

// status is reflected in software title response
Expand Down
13 changes: 10 additions & 3 deletions server/service/integration_mdm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9831,14 +9831,14 @@ func (s *integrationMDMTestSuite) TestVPPApps() {

// Simulate failed installation on the host
cmd, err := mdmDevice.Idle()
var cmdUUID string
var failedCmdUUID string
require.NoError(t, err)
for cmd != nil {
var fullCmd micromdm.CommandPayload
switch cmd.Command.RequestType {
case "InstallApplication":
require.NoError(t, plist.Unmarshal(cmd.Raw, &fullCmd))
cmdUUID = cmd.CommandUUID
failedCmdUUID = cmd.CommandUUID
cmd, err = mdmDevice.Err(cmd.CommandUUID, []mdm.ErrorChain{{ErrorCode: 1234}})
require.NoError(t, err)
}
Expand All @@ -9860,7 +9860,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
mdmHost.DisplayName(),
errApp.Name,
errApp.AdamID,
cmdUUID,
failedCmdUUID,
fleet.SoftwareInstallerFailed,
),
0,
Expand All @@ -9875,6 +9875,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Equal(t, 1, countResp.Count)

// Simulate successful installation on the host
var cmdUUID string
cmd, err = mdmDevice.Idle()
require.NoError(t, err)
for cmd != nil {
Expand Down Expand Up @@ -9924,6 +9925,8 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
require.NotNil(t, *got1.Status)
require.Equal(t, *got1.Status, fleet.SoftwareInstallerInstalled)
require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, cmdUUID)
require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)
require.Equal(t, got2.Name, "App 2")
require.NotNil(t, *got2.Status)
require.Equal(t, *got2.Status, fleet.SoftwareInstallerFailed)
Expand All @@ -9932,6 +9935,8 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Equal(t, got2.AppStoreApp.IconURL, ptr.String(errApp.IconURL))
require.Empty(t, got2.AppStoreApp.Name)
require.Equal(t, got2.AppStoreApp.Version, errApp.LatestVersion)
require.Equal(t, got2.AppStoreApp.LastInstall.CommandUUID, failedCmdUUID)
require.NotNil(t, got2.AppStoreApp.LastInstall.InstalledAt)

// Check with a query
getHostSw = getHostSoftwareResponse{}
Expand All @@ -9947,6 +9952,8 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
require.NotNil(t, *got1.Status)
require.Equal(t, *got1.Status, fleet.SoftwareInstallerInstalled)
require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, cmdUUID)
require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)

// Delete VPP token and check that it's not appearing anymore
s.Do("DELETE", "/api/latest/fleet/mdm/apple/vpp_token", &deleteMDMAppleVPPTokenRequest{}, http.StatusNoContent)
Expand Down

0 comments on commit fda3785

Please sign in to comment.