Software installers: backend cleanup tasks part 1 (#18955)

This commit is contained in:
Martin Angers
2024-05-14 08:37:07 -04:00
committed by GitHub
parent 0debd18673
commit 3579e5a250
6 changed files with 759 additions and 707 deletions
+1 -1
View File
@@ -306,7 +306,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
hsi.host_id = :host_id AND
hsi.pre_install_query_output IS NULL AND
hsi.install_script_exit_code IS NULL
`, softwareInstallerHostStatusNamedQuery("")),
`, softwareInstallerHostStatusNamedQuery("hsi", "")),
}
seconds := int(scripts.MaxServerWaitTime.Seconds())
+19 -14
View File
@@ -1721,33 +1721,38 @@ func (ds *Datastore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]flee
return result, nil
}
// tblAlias is the table alias to use as prefix for the host_script_installs
// column names, no prefix used if empty.
// colAlias is the name to be assigned to the computed status column, pass
// empty to have the value only, no column alias set.
func softwareInstallerHostStatusNamedQuery(colAlias string) string {
func softwareInstallerHostStatusNamedQuery(tblAlias, colAlias string) string {
if tblAlias != "" {
tblAlias += "."
}
if colAlias != "" {
colAlias = " AS " + colAlias
}
return fmt.Sprintf(`
CASE
WHEN hsi.post_install_script_exit_code IS NOT NULL AND
hsi.post_install_script_exit_code = 0 THEN :software_status_installed
WHEN %[1]spost_install_script_exit_code IS NOT NULL AND
%[1]spost_install_script_exit_code = 0 THEN :software_status_installed
WHEN hsi.post_install_script_exit_code IS NOT NULL AND
hsi.post_install_script_exit_code != 0 THEN :software_status_failed
WHEN %[1]spost_install_script_exit_code IS NOT NULL AND
%[1]spost_install_script_exit_code != 0 THEN :software_status_failed
WHEN hsi.install_script_exit_code IS NOT NULL AND
hsi.install_script_exit_code = 0 THEN :software_status_installed
WHEN %[1]sinstall_script_exit_code IS NOT NULL AND
%[1]sinstall_script_exit_code = 0 THEN :software_status_installed
WHEN hsi.install_script_exit_code IS NOT NULL AND
hsi.install_script_exit_code != 0 THEN :software_status_failed
WHEN %[1]sinstall_script_exit_code IS NOT NULL AND
%[1]sinstall_script_exit_code != 0 THEN :software_status_failed
WHEN hsi.pre_install_query_output IS NOT NULL AND
hsi.pre_install_query_output = '' THEN :software_status_failed
WHEN %[1]spre_install_query_output IS NOT NULL AND
%[1]spre_install_query_output = '' THEN :software_status_failed
WHEN hsi.host_id IS NOT NULL THEN :software_status_pending
WHEN %[1]shost_id IS NOT NULL THEN :software_status_pending
ELSE NULL -- not installed from Fleet installer
END %s `, colAlias)
END %[2]s `, tblAlias, colAlias)
}
func (ds *Datastore) ListHostSoftware(ctx context.Context, hostID uint, includeAvailableForInstall bool, opts fleet.ListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error) {
@@ -1792,7 +1797,7 @@ func (ds *Datastore) ListHostSoftware(ctx context.Context, hostID uint, includeA
) OR
-- or software install has been attempted on host
hsi.host_id IS NOT NULL )
`, softwareInstallerHostStatusNamedQuery("status"))
`, softwareInstallerHostStatusNamedQuery("hsi", "status"))
const stmtAvailable = `
SELECT
+33 -60
View File
@@ -197,7 +197,7 @@ SELECT
inst.contents AS install_script,
COALESCE(pisnt.contents, '') AS post_install_script,
COALESCE(st.name, '') AS software_title
FROM
FROM
software_installers si
LEFT OUTER JOIN software_titles st ON st.id = si.title_id
LEFT OUTER JOIN
@@ -206,7 +206,7 @@ FROM
LEFT OUTER JOIN
script_contents pisnt
ON pisnt.id = si.post_install_script_content_id
WHERE
WHERE
si.title_id = ? AND si.global_or_team_id = ?`
var tmID uint
@@ -314,7 +314,7 @@ func (ds *Datastore) InsertSoftwareInstallRequest(ctx context.Context, hostID ui
}
func (ds *Datastore) GetSoftwareInstallResults(ctx context.Context, resultsUUID string) (*fleet.HostSoftwareInstallerResult, error) {
query := `
query := fmt.Sprintf(`
SELECT
hsi.execution_id AS execution_id,
COALESCE(hsi.pre_install_query_output, '') AS pre_install_query_output,
@@ -324,20 +324,7 @@ SELECT
h.computer_name AS host_display_name,
st.name AS software_title,
st.id AS software_title_id,
COALESCE(CASE
WHEN hsi.post_install_script_exit_code IS NOT NULL AND
hsi.post_install_script_exit_code = 0 THEN ? -- installed
WHEN hsi.post_install_script_exit_code IS NOT NULL AND
hsi.post_install_script_exit_code != 0 THEN ? -- failed
WHEN hsi.install_script_exit_code IS NOT NULL AND
hsi.install_script_exit_code = 0 THEN ? -- installed
WHEN hsi.install_script_exit_code IS NOT NULL AND
hsi.install_script_exit_code != 0 THEN ? -- failed
WHEN hsi.pre_install_query_output IS NOT NULL AND
hsi.pre_install_query_output = '' THEN ? -- failed
WHEN hsi.host_id IS NOT NULL THEN ? -- pending
ELSE NULL -- not installed from Fleet installer
END, '') AS status,
COALESCE(%s, '') AS status,
si.filename AS software_package,
h.team_id AS host_team_id,
hsi.user_id AS user_id
@@ -347,11 +334,21 @@ FROM
JOIN software_installers si ON si.id = hsi.software_installer_id
JOIN software_titles st ON si.title_id = st.id
WHERE
hsi.execution_id = ?
`
hsi.execution_id = :execution_id
`, softwareInstallerHostStatusNamedQuery("hsi", ""))
stmt, args, err := sqlx.Named(query, map[string]any{
"execution_id": resultsUUID,
"software_status_failed": fleet.SoftwareInstallerFailed,
"software_status_pending": fleet.SoftwareInstallerPending,
"software_status_installed": fleet.SoftwareInstallerInstalled,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "build named query for get software install results")
}
var dest fleet.HostSoftwareInstallerResult
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, fleet.SoftwareInstallerInstalled, fleet.SoftwareInstallerFailed, fleet.SoftwareInstallerInstalled, fleet.SoftwareInstallerFailed, fleet.SoftwareInstallerFailed, fleet.SoftwareInstallerPending, resultsUUID)
err = sqlx.GetContext(ctx, ds.reader(ctx), &dest, stmt, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("HostSoftwareInstallerResult"), "get host software installer results")
@@ -362,42 +359,18 @@ WHERE
return &dest, nil
}
func tmplNamedSQLCaseHostSoftwareInstallStatus(alias string) string {
return fmt.Sprintf(`
CASE WHEN %[1]s.post_install_script_exit_code IS NOT NULL
AND %[1]s.post_install_script_exit_code = 0 THEN
:installed
WHEN %[1]s.post_install_script_exit_code IS NOT NULL
AND %[1]s.post_install_script_exit_code != 0 THEN
:failed
WHEN %[1]s.install_script_exit_code IS NOT NULL
AND %[1]s.install_script_exit_code = 0 THEN
:installed
WHEN %[1]s.install_script_exit_code IS NOT NULL
AND %[1]s.install_script_exit_code != 0 THEN
:failed
WHEN %[1]s.pre_install_query_output IS NOT NULL
AND %[1]s.pre_install_query_output = '' THEN
:failed
WHEN %[1]s.host_id IS NOT NULL THEN
:pending
ELSE
NULL -- not installed from Fleet installer
END`, alias)
}
func (ds *Datastore) GetSummaryHostSoftwareInstalls(ctx context.Context, installerID uint) (*fleet.SoftwareInstallerStatusSummary, error) {
var dest fleet.SoftwareInstallerStatusSummary
stmt := fmt.Sprintf(`
SELECT
COALESCE(SUM( IF(status = :pending, 1, 0)), 0) AS pending,
COALESCE(SUM( IF(status = :failed, 1, 0)), 0) AS failed,
COALESCE(SUM( IF(status = :installed, 1, 0)), 0) AS installed
COALESCE(SUM( IF(status = :software_status_pending, 1, 0)), 0) AS pending,
COALESCE(SUM( IF(status = :software_status_failed, 1, 0)), 0) AS failed,
COALESCE(SUM( IF(status = :software_status_installed, 1, 0)), 0) AS installed
FROM (
SELECT
software_installer_id,
%s AS status
%s
FROM
host_software_installs hsi
WHERE
@@ -406,16 +379,16 @@ WHERE
SELECT
max(id) -- ensure we use only the most recently created install attempt for each host
FROM host_software_installs
WHERE
WHERE
software_installer_id = :installer_id
GROUP BY
host_id)) s`, tmplNamedSQLCaseHostSoftwareInstallStatus("hsi"))
host_id)) s`, softwareInstallerHostStatusNamedQuery("hsi", "status"))
query, args, err := sqlx.Named(stmt, map[string]interface{}{
"installer_id": installerID,
"pending": fleet.SoftwareInstallerPending,
"failed": fleet.SoftwareInstallerFailed,
"installed": fleet.SoftwareInstallerInstalled,
"installer_id": installerID,
"software_status_pending": fleet.SoftwareInstallerPending,
"software_status_failed": fleet.SoftwareInstallerFailed,
"software_status_installed": fleet.SoftwareInstallerInstalled,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get summary host software installs: named query")
@@ -446,14 +419,14 @@ WHERE
GROUP BY
host_id, software_installer_id)
AND (%s) = :status) hss ON hss.host_id = h.id
`, tmplNamedSQLCaseHostSoftwareInstallStatus("hsi"))
`, softwareInstallerHostStatusNamedQuery("hsi", ""))
return sqlx.Named(stmt, map[string]interface{}{
"status": status,
"installer_id": installerID,
"installed": fleet.SoftwareInstallerInstalled,
"failed": fleet.SoftwareInstallerFailed,
"pending": fleet.SoftwareInstallerPending,
"status": status,
"installer_id": installerID,
"software_status_installed": fleet.SoftwareInstallerInstalled,
"software_status_failed": fleet.SoftwareInstallerFailed,
"software_status_pending": fleet.SoftwareInstallerPending,
})
}
+696 -12
View File
@@ -5,13 +5,16 @@ import (
"context"
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
@@ -8679,14 +8682,20 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
ctx := context.Background()
t := s.T()
// clean up any software titles from previous tests
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM software_titles`)
return err
})
token := "good_token"
host := createHostAndDeviceToken(t, s.ds, token)
host := createOrbitEnrolledHost(t, "linux", "host1", s.ds)
createDeviceTokenForHost(t, s.ds, host.ID, token)
// no software yet
var getHostSw getHostSoftwareResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
require.Len(t, getHostSw.Software, 0)
var getDeviceSw getDeviceSoftwareResponse
res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software", nil, http.StatusOK)
err := json.NewDecoder(res.Body).Decode(&getDeviceSw)
require.NoError(t, err)
require.Len(t, getDeviceSw.Software, 0)
// create some software for that host
software := []fleet.Software{
@@ -8694,20 +8703,20 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
{Name: "foo", Version: "0.0.2", Source: "chrome_extensions"},
{Name: "bar", Version: "0.0.1", Source: "apps"},
}
_, err := s.ds.UpdateHostSoftware(ctx, host.ID, software)
_, err = s.ds.UpdateHostSoftware(ctx, host.ID, software)
require.NoError(t, err)
err = s.ds.ReconcileSoftwareTitles(ctx)
require.NoError(t, err)
var getHostSw getHostSoftwareResponse
getHostSw = getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
require.Len(t, getHostSw.Software, 2) // foo and bar
require.Equal(t, getHostSw.Software[0].Name, "bar")
require.Equal(t, getHostSw.Software[1].Name, "foo")
require.Len(t, getHostSw.Software[1].InstalledVersions, 2)
var getDeviceSw getDeviceSoftwareResponse
res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software", nil, http.StatusOK)
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software", nil, http.StatusOK)
getDeviceSw = getDeviceSoftwareResponse{}
err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
require.NoError(t, err)
require.Len(t, getDeviceSw.Software, 2) // foo and bar
@@ -8715,19 +8724,694 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
require.Equal(t, getDeviceSw.Software[1].Name, "foo")
require.Len(t, getDeviceSw.Software[1].InstalledVersions, 2)
// create a software installer, not installed on the host
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install",
Filename: "ruby.deb",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, "ruby", "deb_packages")
// available installer is returned by user-authenticated endpoint
getHostSw = getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
require.Len(t, getHostSw.Software, 3) // foo, bar and ruby.deb
require.Equal(t, getHostSw.Software[0].Name, "bar")
require.Equal(t, getHostSw.Software[1].Name, "foo")
require.Equal(t, getHostSw.Software[2].Name, "ruby")
require.Len(t, getHostSw.Software[1].InstalledVersions, 2)
require.NotNil(t, getHostSw.Software[2].PackageAvailableForInstall)
require.Equal(t, "ruby.deb", *getHostSw.Software[2].PackageAvailableForInstall)
require.Nil(t, getHostSw.Software[2].Status)
// available installer is not returned by device-authenticated endpoint
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software", nil, http.StatusOK)
getDeviceSw = getDeviceSoftwareResponse{}
err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
require.NoError(t, err)
require.Len(t, getDeviceSw.Software, 2) // foo and bar
require.Equal(t, getDeviceSw.Software[0].Name, "bar")
require.Equal(t, getDeviceSw.Software[1].Name, "foo")
require.Len(t, getDeviceSw.Software[1].InstalledVersions, 2)
require.Nil(t, getDeviceSw.Software[0].PackageAvailableForInstall)
require.Nil(t, getDeviceSw.Software[1].PackageAvailableForInstall)
// request installation on the host
var installResp installSoftwareResponse
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d",
host.ID, titleID), nil, http.StatusAccepted, &installResp)
// still returned by user-authenticated endpoint, now pending
getHostSw = getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw)
require.Len(t, getHostSw.Software, 3) // foo, bar and ruby.deb
require.Equal(t, getHostSw.Software[0].Name, "bar")
require.Equal(t, getHostSw.Software[1].Name, "foo")
require.Equal(t, getHostSw.Software[2].Name, "ruby")
require.Len(t, getHostSw.Software[1].InstalledVersions, 2)
require.NotNil(t, getHostSw.Software[2].PackageAvailableForInstall)
require.Equal(t, "ruby.deb", *getHostSw.Software[2].PackageAvailableForInstall)
require.NotNil(t, getHostSw.Software[2].Status)
require.Equal(t, fleet.SoftwareInstallerPending, *getHostSw.Software[2].Status)
// now returned by device-authenticated endpoin
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software", nil, http.StatusOK)
getDeviceSw = getDeviceSoftwareResponse{}
err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
require.NoError(t, err)
require.Len(t, getDeviceSw.Software, 3) // foo, bar and ruby
require.Equal(t, getDeviceSw.Software[0].Name, "bar")
require.Equal(t, getDeviceSw.Software[1].Name, "foo")
require.Equal(t, getDeviceSw.Software[2].Name, "ruby")
require.Len(t, getDeviceSw.Software[1].InstalledVersions, 2)
require.Nil(t, getDeviceSw.Software[2].PackageAvailableForInstall)
require.NotNil(t, getDeviceSw.Software[2].Status)
require.Equal(t, fleet.SoftwareInstallerPending, *getDeviceSw.Software[2].Status)
// test with a query
getHostSw = getHostSoftwareResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", host.ID), nil, http.StatusOK, &getHostSw, "query", "foo")
require.Len(t, getHostSw.Software, 1) // foo only
require.Equal(t, getHostSw.Software[0].Name, "foo")
require.Len(t, getHostSw.Software[0].InstalledVersions, 2)
res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/software?query=bar", nil, http.StatusOK)
getDeviceSw = getDeviceSoftwareResponse{}
err = json.NewDecoder(res.Body).Decode(&getDeviceSw)
require.NoError(t, err)
require.Len(t, getDeviceSw.Software, 1) // bar only
require.Equal(t, getDeviceSw.Software[0].Name, "bar")
require.Len(t, getDeviceSw.Software[0].InstalledVersions, 1)
}
// TODO(mna): more advanced integration tests with Software Installers once the APIs are in place.
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerUploadDownloadAndDelete() {
t := s.T()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
var expectBytes []byte
var expectLen int
f := openFile("ruby.deb")
st, err := f.Stat()
require.NoError(t, err)
expectLen = int(st.Size())
require.Equal(t, expectLen, 11340)
expectBytes = make([]byte, expectLen)
n, err := f.Read(expectBytes)
require.NoError(t, err)
require.Equal(t, n, expectLen)
f.Close()
checkDownloadResponse := func(t *testing.T, r *http.Response, expectedFilename string) {
require.Equal(t, "application/octet-stream", r.Header.Get("Content-Type"))
require.Equal(t, fmt.Sprintf(`attachment;filename="%s"`, expectedFilename), r.Header.Get("Content-Disposition"))
require.NotZero(t, r.ContentLength)
require.Equal(t, expectLen, int(r.ContentLength))
b, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, expectLen, len(b))
require.Equal(t, expectBytes, b)
}
checkSoftwareTitle := func(t *testing.T, title string, source string) uint {
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id, `SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''`, title, source)
})
return id
}
checkScriptContentsID := func(t *testing.T, id uint, expectedContents string) {
var contents string
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &contents, `SELECT contents FROM script_contents WHERE id = ?`, id)
})
require.Equal(t, expectedContents, contents)
}
checkSoftwareInstaller := func(t *testing.T, payload *fleet.UploadSoftwareInstallerPayload) (installerID uint, titleID uint) {
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
var tid uint
if payload.TeamID != nil {
tid = *payload.TeamID
}
return sqlx.GetContext(context.Background(), q, &id, `SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`, tid, payload.Filename)
})
require.NotZero(t, id)
meta, err := s.ds.GetSoftwareInstallerMetadata(context.Background(), id)
require.NoError(t, err)
if payload.TeamID != nil {
require.Equal(t, *payload.TeamID, *meta.TeamID)
} else {
require.Nil(t, meta.TeamID)
}
checkScriptContentsID(t, meta.InstallScriptContentID, payload.InstallScript)
if payload.PostInstallScript != "" {
require.NotNil(t, meta.PostInstallScriptContentID)
checkScriptContentsID(t, *meta.PostInstallScriptContentID, payload.PostInstallScript)
} else {
require.Nil(t, meta.PostInstallScriptContentID)
}
require.Equal(t, payload.PreInstallQuery, meta.PreInstallQuery)
require.Equal(t, payload.StorageID, meta.StorageID)
require.Equal(t, payload.Filename, meta.Name)
require.Equal(t, payload.Version, meta.Version)
require.Equal(t, checkSoftwareTitle(t, payload.Title, "deb_packages"), *meta.TitleID)
require.NotZero(t, meta.UploadedAt)
return meta.InstallerID, *meta.TitleID
}
t.Run("upload no team software installer", func(t *testing.T) {
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "some install script",
PreInstallQuery: "some pre install query",
PostInstallScript: "some post install script",
Filename: "ruby.deb",
// additional fields below are pre-populated so we can re-use the payload later for the test assertions
Title: "ruby",
Version: "1:2.5.1",
Source: "deb_packages",
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null}`, 0)
// check the software installer
_, titleID := checkSoftwareInstaller(t, payload)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
// download the installer
s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/%d/package?alt=media", titleID), nil, http.StatusBadRequest)
// delete the installer
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/%d/package", titleID), nil, http.StatusBadRequest)
})
t.Run("create team software installer", func(t *testing.T) {
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
payload := &fleet.UploadSoftwareInstallerPayload{
TeamID: &createTeamResp.Team.ID,
InstallScript: "another install script",
PreInstallQuery: "another pre install query",
PostInstallScript: "another post install script",
Filename: "ruby.deb",
// additional fields below are pre-populated so we can re-use the payload later for the test assertions
Title: "ruby",
Version: "1:2.5.1",
Source: "deb_packages",
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
// check the software installer
installerID, titleID := checkSoftwareInstaller(t, payload)
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
// download the installer
r := s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/%d/package?alt=media", titleID), nil, http.StatusOK, "team_id", fmt.Sprintf("%d", *payload.TeamID))
checkDownloadResponse(t, r, payload.Filename)
// create an orbit host, assign to team and request to download the installer
host := createOrbitEnrolledHost(t, "windows", "orbit-host-team", s.ds)
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), &createTeamResp.Team.ID, []uint{host.ID}))
r = s.Do("POST", "/api/fleet/orbit/software_install/package?alt=media", orbitDownloadSoftwareInstallerRequest{
InstallerID: installerID,
OrbitNodeKey: *host.OrbitNodeKey,
}, http.StatusOK)
checkDownloadResponse(t, r, payload.Filename)
// delete the installer
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/%d/package", titleID), nil, http.StatusNoContent, "team_id", fmt.Sprintf("%d", *payload.TeamID))
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
})
}
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerNewInstallRequestPlatformValidation() {
t := s.T()
hostsByPlatform := map[string]*fleet.Host{
"linux": nil, "darwin": nil, "windows": nil,
}
tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: t.Name(),
Description: "desc",
})
require.NoError(t, err)
for platform := range hostsByPlatform {
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + uuid.New().String()),
NodeKey: ptr.String(t.Name() + uuid.New().String()),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: platform,
})
require.NoError(t, err)
setOrbitEnrollment(t, h, s.ds)
err = s.ds.AddHostsToTeam(context.Background(), &tm.ID, []uint{h.ID})
require.NoError(t, err)
hostsByPlatform[platform] = h
}
softwareTitles := map[string]uint{
"deb": 0, "msi": 0, "exe": 0, "pkg": 0,
}
for kind := range softwareTitles {
// TODO(roberto): we need real binaries for exe, msi and pkg to
// perform the API calls.
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
ctx := context.Background()
installScript := fmt.Sprintf(`echo '%s'`, kind)
res, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`, installScript, installScript)
if err != nil {
return err
}
scriptContentID, _ := res.LastInsertId()
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('foo', ?)`, kind)
if err != nil {
return err
}
titleID, _ := res.LastInsertId()
softwareTitles[kind] = uint(titleID)
_, err = q.ExecContext(ctx, `
INSERT INTO software_installers
(title_id, filename, version, install_script_content_id, storage_id, team_id, global_or_team_id, pre_install_query)
VALUES
(?, ?, ?, ?, unhex(?), ?, ?, ?)`,
titleID, fmt.Sprintf("installer.%s", kind), "v1.0.0", scriptContentID, hex.EncodeToString([]byte("test")), tm.ID, tm.ID, "foo")
return err
})
}
testCases := []struct {
platform string
supportedInstallers []string
}{
{"windows", []string{"exe", "msi"}},
{"darwin", []string{"pkg"}},
{"linux", []string{"deb"}},
}
for _, tc := range testCases {
for platform, host := range hostsByPlatform {
for _, kind := range tc.supportedInstallers {
wantStatus := http.StatusAccepted
if tc.platform != platform {
wantStatus = http.StatusBadRequest
}
var resp installSoftwareResponse
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", host.ID, softwareTitles[kind]), nil, wantStatus, &resp)
}
}
}
}
func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
t := s.T()
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
teamID := &createTeamResp.Team.ID
var resp installSoftwareResponse
// non-existent host
s.DoJSON("POST", "/api/latest/fleet/hosts/1/software/install/1", nil, http.StatusNotFound, &resp)
// create a host that doesn't have fleetd installed
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + uuid.New().String()),
NodeKey: ptr.String(t.Name() + uuid.New().String()),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "linux",
})
require.NoError(t, err)
err = s.ds.AddHostsToTeam(context.Background(), teamID, []uint{h.ID})
require.NoError(t, err)
// request fails
resp = installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/1", h.ID), nil, http.StatusUnprocessableEntity, &resp)
// host installs fleetd
setOrbitEnrollment(t, h, s.ds)
// TODO(roberto) setOrbitEnrollment is a helper function that silently
// sets the team_id to NULL. We need to refactor it to accept a
// parameter with an optional team value.
err = s.ds.AddHostsToTeam(context.Background(), teamID, []uint{h.ID})
require.NoError(t, err)
// request fails because of non-existent title
resp = installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/1", h.ID), nil, http.StatusBadRequest, &resp)
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "another install script",
PreInstallQuery: "another pre install query",
PostInstallScript: "another post install script",
Filename: "ruby.deb",
Title: "ruby",
TeamID: teamID,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
// install script request succeeds
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
resp = installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h.ID, titleID), nil, http.StatusAccepted, &resp)
// Get the results, should be pending
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)
installUUID := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
gsirr := getSoftwareInstallResultsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
require.NoError(t, gsirr.Err)
require.NotNil(t, gsirr.Results)
results := gsirr.Results
require.Equal(t, installUUID, results.InstallUUID)
require.Equal(t, fleet.SoftwareInstallerPending, results.Status)
// status is reflected in software title response
titleResp := getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", strconv.Itoa(int(*teamID)))
// TODO: confirm expected behavior of the title response host counts (unspecified)
require.Zero(t, titleResp.SoftwareTitle.HostsCount)
require.Nil(t, titleResp.SoftwareTitle.CountsUpdatedAt)
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage)
require.Equal(t, "ruby.deb", titleResp.SoftwareTitle.SoftwarePackage.Name)
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage.Status)
require.Equal(t, fleet.SoftwareInstallerStatusSummary{
Installed: 0,
Pending: 1,
Failed: 0,
}, *titleResp.SoftwareTitle.SoftwarePackage.Status)
// status is reflected in list hosts responses and counts when filtering by software title and status
var listResp listHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 1)
require.Equal(t, h.ID, listResp.Hosts[0].ID)
var countResp countHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 1, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 0, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 0, countResp.Count)
var labelResp createLabelResponse
s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{
Name: "test",
Hosts: []string{h.Hostname},
}}, http.StatusOK, &labelResp)
require.NotZero(t, labelResp.Label.ID)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp)
require.Len(t, listResp.Hosts, 1)
require.Equal(t, h.ID, listResp.Hosts[0].ID)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 1, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 1)
require.Equal(t, h.ID, listResp.Hosts[0].ID)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 1, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 0, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 0, countResp.Count)
// filter validations
r := s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "uninstalled")
require.Contains(t, extractServerErrorText(r.Body), "Invalid software_status")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed")
require.Contains(t, extractServerErrorText(r.Body), "Missing software_title_id")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "software_title_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Missing team_id")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "team_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Missing software_title_id")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "team_id", "1", "software_title_id", "1", "software_version_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Invalid parameters. The combination of software_version_id and software_title_id is not allowed.")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "team_id", "1", "software_title_id", "1", "software_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Invalid parameters. The combination of software_id and software_title_id is not allowed.")
// TODO(roberto): once we have endpoints to retrieve installers,
// request them using the orbit node key
// TODO(sarah): test other statuses once we have endpoints to set results via orbit
}
func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
ctx := context.Background()
t := s.T()
host := createOrbitEnrolledHost(t, "linux", "", s.ds)
// create a software installer and some host install requests
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install script",
PreInstallQuery: "pre install query",
PostInstallScript: "post install script",
Filename: "ruby.deb",
Title: "ruby",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
latestInstallUUID := func() string {
var id string
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &id, `SELECT execution_id FROM host_software_installs ORDER BY id DESC LIMIT 1`)
})
return id
}
// create some install requests for the host
installUUIDs := make([]string, 3)
for i := 0; i < len(installUUIDs); i++ {
resp := installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleID), nil, http.StatusAccepted, &resp)
installUUIDs[i] = latestInstallUUID()
}
type result struct {
HostID uint
InstallUUID string
Status fleet.SoftwareInstallerStatus
}
checkResults := func(want result) {
var resp getSoftwareInstallResultsResponse
s.DoJSON("GET", "/api/v1/fleet/software/install/results/"+want.InstallUUID, nil, http.StatusOK, &resp)
assert.Equal(t, want.HostID, resp.Results.HostID)
assert.Equal(t, want.InstallUUID, resp.Results.InstallUUID)
assert.Equal(t, want.Status, resp.Results.Status)
}
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"pre_install_condition_output": "1",
"install_script_exit_code": 1,
"install_script_output": "failed"
}`, *host.OrbitNodeKey, installUUIDs[0])),
http.StatusNoContent)
checkResults(result{
HostID: host.ID,
InstallUUID: installUUIDs[0],
Status: fleet.SoftwareInstallerFailed,
})
wantAct := fleet.ActivityTypeInstalledSoftware{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
SoftwareTitle: payload.Title,
InstallUUID: installUUIDs[0],
Status: string(fleet.SoftwareInstallerFailed),
}
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"pre_install_condition_output": ""
}`, *host.OrbitNodeKey, installUUIDs[1])),
http.StatusNoContent)
checkResults(result{
HostID: host.ID,
InstallUUID: installUUIDs[1],
Status: fleet.SoftwareInstallerFailed,
})
wantAct.InstallUUID = installUUIDs[1]
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"pre_install_condition_output": "1",
"install_script_exit_code": 0,
"install_script_output": "success",
"post_install_script_exit_code": 0,
"post_install_script_output": "ok"
}`, *host.OrbitNodeKey, installUUIDs[2])),
http.StatusNoContent)
checkResults(result{
HostID: host.ID,
InstallUUID: installUUIDs[2],
Status: fleet.SoftwareInstallerInstalled,
})
wantAct.InstallUUID = installUUIDs[2]
wantAct.Status = string(fleet.SoftwareInstallerInstalled)
lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
// non-existing installation uuid
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": "uuid-no-such",
"pre_install_condition_output": ""
}`, *host.OrbitNodeKey)),
http.StatusNotFound)
// no new activity created
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), lastActID)
}
func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(payload *fleet.UploadSoftwareInstallerPayload, expectedStatus int, expectedError string) {
t := s.T()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
f := openFile(payload.Filename)
defer f.Close()
payload.InstallerFile = f
var b bytes.Buffer
w := multipart.NewWriter(&b)
// add the software field
fw, err := w.CreateFormFile("software", payload.Filename)
require.NoError(t, err)
n, err := io.Copy(fw, payload.InstallerFile)
require.NoError(t, err)
require.NotZero(t, n)
// add the team_id field
if payload.TeamID != nil {
require.NoError(t, w.WriteField("team_id", fmt.Sprintf("%d", *payload.TeamID)))
}
// add the remaining fields
require.NoError(t, w.WriteField("install_script", payload.InstallScript))
require.NoError(t, w.WriteField("pre_install_query", payload.PreInstallQuery))
require.NoError(t, w.WriteField("post_install_script", payload.PostInstallScript))
w.Close()
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
"Accept": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", s.token),
}
r := s.DoRawWithHeaders("POST", "/api/latest/fleet/software/package", b.Bytes(), expectedStatus, headers)
if expectedError != "" {
errMsg := extractServerErrorText(r.Body)
require.Contains(t, errMsg, expectedError)
}
}
func getSoftwareTitleID(t *testing.T, ds *mysql.Datastore, title, source string) uint {
var id uint
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id, `SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''`, title, source)
})
return id
}
func genDistributedReqWithPolicyResults(host *fleet.Host, policyResults map[uint]*bool) submitDistributedQueryResultsRequestShim {
-617
View File
@@ -6,7 +6,6 @@ import (
"crypto/x509"
"database/sql"
"encoding/base64"
"encoding/hex"
"encoding/json"
"encoding/xml"
"errors"
@@ -363,12 +362,6 @@ func (s *integrationMDMTestSuite) TearDownTest() {
_, err := tx.ExecContext(ctx, "DELETE FROM host_mdm_apple_declarations")
return err
})
// clear any lingering software installers
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM software_installers")
return err
})
}
func (s *integrationMDMTestSuite) mockDEPResponse(handler http.Handler) {
@@ -8466,613 +8459,3 @@ func (s *integrationMDMTestSuite) TestIsServerBitlockerStatus() {
require.NotNil(t, hr.Host.MDM.OSSettings.DiskEncryption.Status)
require.Equal(t, fleet.DiskEncryptionEnforcing, *hr.Host.MDM.OSSettings.DiskEncryption.Status)
}
func (s *integrationMDMTestSuite) TestSoftwareInstallerUploadDownloadAndDelete() {
t := s.T()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
var expectBytes []byte
var expectLen int
f := openFile("ruby.deb")
st, err := f.Stat()
require.NoError(t, err)
expectLen = int(st.Size())
require.Equal(t, expectLen, 11340)
expectBytes = make([]byte, expectLen)
n, err := f.Read(expectBytes)
require.NoError(t, err)
require.Equal(t, n, expectLen)
f.Close()
checkDownloadResponse := func(t *testing.T, r *http.Response, expectedFilename string) {
require.Equal(t, "application/octet-stream", r.Header.Get("Content-Type"))
require.Equal(t, fmt.Sprintf(`attachment;filename="%s"`, expectedFilename), r.Header.Get("Content-Disposition"))
require.NotZero(t, r.ContentLength)
require.Equal(t, expectLen, int(r.ContentLength))
b, err := io.ReadAll(r.Body)
require.NoError(t, err)
require.Equal(t, expectLen, len(b))
require.Equal(t, expectBytes, b)
}
checkSoftwareTitle := func(t *testing.T, title string, source string) uint {
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id, `SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''`, title, source)
})
return id
}
checkScriptContentsID := func(t *testing.T, id uint, expectedContents string) {
var contents string
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &contents, `SELECT contents FROM script_contents WHERE id = ?`, id)
})
require.Equal(t, expectedContents, contents)
}
checkSoftwareInstaller := func(t *testing.T, payload *fleet.UploadSoftwareInstallerPayload) (installerID uint, titleID uint) {
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
var tid uint
if payload.TeamID != nil {
tid = *payload.TeamID
}
return sqlx.GetContext(context.Background(), q, &id, `SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`, tid, payload.Filename)
})
require.NotZero(t, id)
meta, err := s.ds.GetSoftwareInstallerMetadata(context.Background(), id)
require.NoError(t, err)
if payload.TeamID != nil {
require.Equal(t, *payload.TeamID, *meta.TeamID)
} else {
require.Nil(t, meta.TeamID)
}
checkScriptContentsID(t, meta.InstallScriptContentID, payload.InstallScript)
if payload.PostInstallScript != "" {
require.NotNil(t, meta.PostInstallScriptContentID)
checkScriptContentsID(t, *meta.PostInstallScriptContentID, payload.PostInstallScript)
} else {
require.Nil(t, meta.PostInstallScriptContentID)
}
require.Equal(t, payload.PreInstallQuery, meta.PreInstallQuery)
require.Equal(t, payload.StorageID, meta.StorageID)
require.Equal(t, payload.Filename, meta.Name)
require.Equal(t, payload.Version, meta.Version)
require.Equal(t, checkSoftwareTitle(t, payload.Title, "deb_packages"), *meta.TitleID)
require.NotZero(t, meta.UploadedAt)
return meta.InstallerID, *meta.TitleID
}
t.Run("upload no team software installer", func(t *testing.T) {
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "some install script",
PreInstallQuery: "some pre install query",
PostInstallScript: "some post install script",
Filename: "ruby.deb",
// additional fields below are pre-populated so we can re-use the payload later for the test assertions
Title: "ruby",
Version: "1:2.5.1",
Source: "deb_packages",
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), `{"software_title": "ruby", "software_package": "ruby.deb", "team_name": null, "team_id": null}`, 0)
// check the software installer
_, titleID := checkSoftwareInstaller(t, payload)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
// download the installer
s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/%d/package?alt=media", titleID), nil, http.StatusBadRequest)
// delete the installer
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/%d/package", titleID), nil, http.StatusBadRequest)
})
t.Run("create team software installer", func(t *testing.T) {
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
payload := &fleet.UploadSoftwareInstallerPayload{
TeamID: &createTeamResp.Team.ID,
InstallScript: "another install script",
PreInstallQuery: "another pre install query",
PostInstallScript: "another post install script",
Filename: "ruby.deb",
// additional fields below are pre-populated so we can re-use the payload later for the test assertions
Title: "ruby",
Version: "1:2.5.1",
Source: "deb_packages",
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
// check the software installer
installerID, titleID := checkSoftwareInstaller(t, payload)
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeAddedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
// upload again fails
s.uploadSoftwareInstaller(payload, http.StatusConflict, "already exists")
// download the installer
r := s.Do("GET", fmt.Sprintf("/api/latest/fleet/software/%d/package?alt=media", titleID), nil, http.StatusOK, "team_id", fmt.Sprintf("%d", *payload.TeamID))
checkDownloadResponse(t, r, payload.Filename)
// create an orbit host, assign to team and request to download the installer
host := createOrbitEnrolledHost(t, "windows", "orbit-host-team", s.ds)
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), &createTeamResp.Team.ID, []uint{host.ID}))
r = s.Do("POST", "/api/fleet/orbit/software_install/package?alt=media", orbitDownloadSoftwareInstallerRequest{
InstallerID: installerID,
OrbitNodeKey: *host.OrbitNodeKey,
}, http.StatusOK)
checkDownloadResponse(t, r, payload.Filename)
// delete the installer
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/%d/package", titleID), nil, http.StatusNoContent, "team_id", fmt.Sprintf("%d", *payload.TeamID))
// check activity
s.lastActivityOfTypeMatches(fleet.ActivityTypeDeletedSoftware{}.ActivityName(), fmt.Sprintf(`{"software_title": "ruby", "software_package": "ruby.deb", "team_name": "%s", "team_id": %d}`, createTeamResp.Team.Name, createTeamResp.Team.ID), 0)
})
}
func (s *integrationMDMTestSuite) TestSoftwareInstallerNewInstallRequestPlatformValidation() {
t := s.T()
hostsByPlatform := map[string]*fleet.Host{
"linux": nil, "darwin": nil, "windows": nil,
}
tm, err := s.ds.NewTeam(context.Background(), &fleet.Team{
Name: t.Name(),
Description: "desc",
})
require.NoError(t, err)
for platform := range hostsByPlatform {
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + uuid.New().String()),
NodeKey: ptr.String(t.Name() + uuid.New().String()),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: platform,
})
require.NoError(t, err)
setOrbitEnrollment(t, h, s.ds)
err = s.ds.AddHostsToTeam(context.Background(), &tm.ID, []uint{h.ID})
require.NoError(t, err)
hostsByPlatform[platform] = h
}
softwareTitles := map[string]uint{
"deb": 0, "msi": 0, "exe": 0, "pkg": 0,
}
for kind := range softwareTitles {
// TODO(roberto): we need real binaries for exe, msi and pkg to
// perform the API calls.
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
ctx := context.Background()
installScript := fmt.Sprintf(`echo '%s'`, kind)
res, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`, installScript, installScript)
if err != nil {
return err
}
scriptContentID, _ := res.LastInsertId()
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('foo', ?)`, kind)
if err != nil {
return err
}
titleID, _ := res.LastInsertId()
softwareTitles[kind] = uint(titleID)
_, err = q.ExecContext(ctx, `
INSERT INTO software_installers
(title_id, filename, version, install_script_content_id, storage_id, team_id, global_or_team_id, pre_install_query)
VALUES
(?, ?, ?, ?, unhex(?), ?, ?, ?)`,
titleID, fmt.Sprintf("installer.%s", kind), "v1.0.0", scriptContentID, hex.EncodeToString([]byte("test")), tm.ID, tm.ID, "foo")
return err
})
}
testCases := []struct {
platform string
supportedInstallers []string
}{
{"windows", []string{"exe", "msi"}},
{"darwin", []string{"pkg"}},
{"linux", []string{"deb"}},
}
for _, tc := range testCases {
for platform, host := range hostsByPlatform {
for _, kind := range tc.supportedInstallers {
wantStatus := http.StatusAccepted
if tc.platform != platform {
wantStatus = http.StatusBadRequest
}
var resp installSoftwareResponse
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", host.ID, softwareTitles[kind]), nil, wantStatus, &resp)
}
}
}
}
func (s *integrationMDMTestSuite) TestSoftwareInstallerHostRequests() {
t := s.T()
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
teamID := &createTeamResp.Team.ID
var resp installSoftwareResponse
// non-existent host
s.DoJSON("POST", "/api/latest/fleet/hosts/1/software/install/1", nil, http.StatusNotFound, &resp)
// create a host that doesn't have fleetd installed
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
LabelUpdatedAt: time.Now(),
PolicyUpdatedAt: time.Now(),
SeenTime: time.Now().Add(-1 * time.Minute),
OsqueryHostID: ptr.String(t.Name() + uuid.New().String()),
NodeKey: ptr.String(t.Name() + uuid.New().String()),
Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
Platform: "linux",
})
require.NoError(t, err)
err = s.ds.AddHostsToTeam(context.Background(), teamID, []uint{h.ID})
require.NoError(t, err)
// request fails
resp = installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/1", h.ID), nil, http.StatusUnprocessableEntity, &resp)
// host installs fleetd
setOrbitEnrollment(t, h, s.ds)
// TODO(roberto) setOrbitEnrollment is a helper function that silently
// sets the team_id to NULL. We need to refactor it to accept a
// parameter with an optional team value.
err = s.ds.AddHostsToTeam(context.Background(), teamID, []uint{h.ID})
require.NoError(t, err)
// request fails because of non-existent title
resp = installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/1", h.ID), nil, http.StatusBadRequest, &resp)
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "another install script",
PreInstallQuery: "another pre install query",
PostInstallScript: "another post install script",
Filename: "ruby.deb",
Title: "ruby",
TeamID: teamID,
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
// install script request succeeds
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
resp = installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h.ID, titleID), nil, http.StatusAccepted, &resp)
// Get the results, should be pending
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)
installUUID := getHostSoftwareResp.Software[0].LastInstall.InstallUUID
gsirr := getSoftwareInstallResultsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
require.NoError(t, gsirr.Err)
require.NotNil(t, gsirr.Results)
results := gsirr.Results
require.Equal(t, installUUID, results.InstallUUID)
require.Equal(t, fleet.SoftwareInstallerPending, results.Status)
// status is reflected in software title response
titleResp := getSoftwareTitleResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", strconv.Itoa(int(*teamID)))
// TODO: confirm expected behavior of the title response host counts (unspecified)
require.Zero(t, titleResp.SoftwareTitle.HostsCount)
require.Nil(t, titleResp.SoftwareTitle.CountsUpdatedAt)
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage)
require.Equal(t, "ruby.deb", titleResp.SoftwareTitle.SoftwarePackage.Name)
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage.Status)
require.Equal(t, fleet.SoftwareInstallerStatusSummary{
Installed: 0,
Pending: 1,
Failed: 0,
}, *titleResp.SoftwareTitle.SoftwarePackage.Status)
// status is reflected in list hosts responses and counts when filtering by software title and status
var listResp listHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 1)
require.Equal(t, h.ID, listResp.Hosts[0].ID)
var countResp countHostsResponse
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 1, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 0, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Equal(t, 0, countResp.Count)
var labelResp createLabelResponse
s.DoJSON("POST", "/api/latest/fleet/labels", &createLabelRequest{fleet.LabelPayload{
Name: "test",
Hosts: []string{h.Hostname},
}}, http.StatusOK, &labelResp)
require.NotZero(t, labelResp.Label.ID)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp)
require.Len(t, listResp.Hosts, 1)
require.Equal(t, h.ID, listResp.Hosts[0].ID)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 1, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 1)
require.Equal(t, h.ID, listResp.Hosts[0].ID)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 1, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "installed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 0, countResp.Count)
listResp = listHostsResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/labels/%d/hosts", labelResp.Label.ID), nil, http.StatusOK, &listResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)))
require.Len(t, listResp.Hosts, 0)
countResp = countHostsResponse{}
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "failed", "team_id", strconv.Itoa(int(*teamID)), "software_title_id", strconv.Itoa(int(titleID)), "label_id", strconv.Itoa(int(labelResp.Label.ID)))
require.Equal(t, 0, countResp.Count)
// filter validations
r := s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "uninstalled")
require.Contains(t, extractServerErrorText(r.Body), "Invalid software_status")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed")
require.Contains(t, extractServerErrorText(r.Body), "Missing software_title_id")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "software_title_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Missing team_id")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "team_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Missing software_title_id")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "team_id", "1", "software_title_id", "1", "software_version_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Invalid parameters. The combination of software_version_id and software_title_id is not allowed.")
r = s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusBadRequest, "software_status", "installed", "team_id", "1", "software_title_id", "1", "software_id", "1")
require.Contains(t, extractServerErrorText(r.Body), "Invalid parameters. The combination of software_id and software_title_id is not allowed.")
// TODO(roberto): once we have endpoints to retrieve installers,
// request them using the orbit node key
// TODO(sarah): test other statuses once we have endpoints to set results via orbit
}
func (s *integrationMDMTestSuite) TestHostSoftwareInstallResult() {
ctx := context.Background()
t := s.T()
host := createOrbitEnrolledHost(t, "linux", "", s.ds)
// create a software installer and some host install requests
payload := &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install script",
PreInstallQuery: "pre install query",
PostInstallScript: "post install script",
Filename: "ruby.deb",
Title: "ruby",
}
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
latestInstallUUID := func() string {
var id string
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &id, `SELECT execution_id FROM host_software_installs ORDER BY id DESC LIMIT 1`)
})
return id
}
// create some install requests for the host
installUUIDs := make([]string, 3)
for i := 0; i < len(installUUIDs); i++ {
resp := installSoftwareResponse{}
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleID), nil, http.StatusAccepted, &resp)
installUUIDs[i] = latestInstallUUID()
}
type result struct {
HostID uint
InstallUUID string
Status fleet.SoftwareInstallerStatus
}
checkResults := func(want result) {
var resp getSoftwareInstallResultsResponse
s.DoJSON("GET", "/api/v1/fleet/software/install/results/"+want.InstallUUID, nil, http.StatusOK, &resp)
assert.Equal(t, want.HostID, resp.Results.HostID)
assert.Equal(t, want.InstallUUID, resp.Results.InstallUUID)
assert.Equal(t, want.Status, resp.Results.Status)
}
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"pre_install_condition_output": "1",
"install_script_exit_code": 1,
"install_script_output": "failed"
}`, *host.OrbitNodeKey, installUUIDs[0])),
http.StatusNoContent)
checkResults(result{
HostID: host.ID,
InstallUUID: installUUIDs[0],
Status: fleet.SoftwareInstallerFailed,
})
wantAct := fleet.ActivityTypeInstalledSoftware{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
SoftwareTitle: payload.Title,
InstallUUID: installUUIDs[0],
Status: string(fleet.SoftwareInstallerFailed),
}
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"pre_install_condition_output": ""
}`, *host.OrbitNodeKey, installUUIDs[1])),
http.StatusNoContent)
checkResults(result{
HostID: host.ID,
InstallUUID: installUUIDs[1],
Status: fleet.SoftwareInstallerFailed,
})
wantAct.InstallUUID = installUUIDs[1]
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"pre_install_condition_output": "1",
"install_script_exit_code": 0,
"install_script_output": "success",
"post_install_script_exit_code": 0,
"post_install_script_output": "ok"
}`, *host.OrbitNodeKey, installUUIDs[2])),
http.StatusNoContent)
checkResults(result{
HostID: host.ID,
InstallUUID: installUUIDs[2],
Status: fleet.SoftwareInstallerInstalled,
})
wantAct.InstallUUID = installUUIDs[2]
wantAct.Status = string(fleet.SoftwareInstallerInstalled)
lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
// non-existing installation uuid
s.Do("POST", "/api/fleet/orbit/software_install/result",
json.RawMessage(fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": "uuid-no-such",
"pre_install_condition_output": ""
}`, *host.OrbitNodeKey)),
http.StatusNotFound)
// no new activity created
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), lastActID)
}
func (s *integrationMDMTestSuite) uploadSoftwareInstaller(payload *fleet.UploadSoftwareInstallerPayload, expectedStatus int, expectedError string) {
t := s.T()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
f := openFile(payload.Filename)
defer f.Close()
payload.InstallerFile = f
var b bytes.Buffer
w := multipart.NewWriter(&b)
// add the software field
fw, err := w.CreateFormFile("software", payload.Filename)
require.NoError(t, err)
n, err := io.Copy(fw, payload.InstallerFile)
require.NoError(t, err)
require.NotZero(t, n)
// add the team_id field
if payload.TeamID != nil {
require.NoError(t, w.WriteField("team_id", fmt.Sprintf("%d", *payload.TeamID)))
}
// add the remaining fields
require.NoError(t, w.WriteField("install_script", payload.InstallScript))
require.NoError(t, w.WriteField("pre_install_query", payload.PreInstallQuery))
require.NoError(t, w.WriteField("post_install_script", payload.PostInstallScript))
w.Close()
headers := map[string]string{
"Content-Type": w.FormDataContentType(),
"Accept": "application/json",
"Authorization": fmt.Sprintf("Bearer %s", s.token),
}
r := s.DoRawWithHeaders("POST", "/api/latest/fleet/software/package", b.Bytes(), expectedStatus, headers)
if expectedError != "" {
errMsg := extractServerErrorText(r.Body)
require.Contains(t, errMsg, expectedError)
}
}
func getSoftwareTitleID(t *testing.T, ds *mysql.Datastore, title, source string) uint {
var id uint
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id, `SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''`, title, source)
})
return id
}
+10 -3
View File
@@ -118,8 +118,11 @@ func (ts *withServer) commonTearDownTest(t *testing.T) {
require.NoError(t, ts.ds.DeleteHost(ctx, host.ID))
}
// recalculate software counts will remove the software entries
require.NoError(t, ts.ds.SyncHostsSoftware(context.Background(), time.Now()))
// clean up any software installers
mysql.ExecAdhocSQL(t, ts.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `DELETE FROM software_installers`)
return err
})
lbls, err := ts.ds.ListLabels(ctx, fleet.TeamFilter{}, fleet.ListOptions{})
require.NoError(t, err)
@@ -176,9 +179,13 @@ func (ts *withServer) commonTearDownTest(t *testing.T) {
require.NoError(t, err)
}
// SyncHostsSoftware performs a cleanup.
// Do the software/titles cleanup.
err = ts.ds.SyncHostsSoftware(ctx, time.Now())
require.NoError(t, err)
err = ts.ds.ReconcileSoftwareTitles(ctx)
require.NoError(t, err)
err = ts.ds.SyncHostsSoftwareTitles(ctx, time.Now())
require.NoError(t, err)
// delete orphaned scripts
mysql.ExecAdhocSQL(t, ts.ds, func(q sqlx.ExtContext) error {