fleet/server/datastore/mysql/software_installers.go
Ian Littman 8258d481a1
Clear pending (un)installs when installers are deleted (#23427)
For unreleased bug #23350, introduced because we're no longer
cascade-deleting _all_ related installs (related to #22996, #21654,
#22087)

# Checklist for submitter

- [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
2024-10-31 18:04:06 -05:00

1320 lines
42 KiB
Go

package mysql
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
)
func (ds *Datastore) ListPendingSoftwareInstalls(ctx context.Context, hostID uint) ([]string, error) {
const stmt = `
SELECT
execution_id
FROM
host_software_installs
WHERE
host_id = ?
AND
status = ?
ORDER BY
created_at ASC
`
var results []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, hostID, fleet.SoftwareInstallPending); err != nil {
return nil, ctxerr.Wrap(ctx, err, "list pending software installs")
}
return results, nil
}
func (ds *Datastore) GetSoftwareInstallDetails(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error) {
const stmt = `
SELECT
hsi.host_id AS host_id,
hsi.execution_id AS execution_id,
hsi.software_installer_id AS installer_id,
hsi.self_service AS self_service,
COALESCE(si.pre_install_query, '') AS pre_install_condition,
inst.contents AS install_script,
uninst.contents AS uninstall_script,
COALESCE(pisnt.contents, '') AS post_install_script
FROM
host_software_installs hsi
INNER JOIN
software_installers si
ON hsi.software_installer_id = si.id
LEFT OUTER JOIN
script_contents inst
ON inst.id = si.install_script_content_id
LEFT OUTER JOIN
script_contents uninst
ON uninst.id = si.uninstall_script_content_id
LEFT OUTER JOIN
script_contents pisnt
ON pisnt.id = si.post_install_script_content_id
WHERE
hsi.execution_id = ?`
result := &fleet.SoftwareInstallDetails{}
if err := sqlx.GetContext(ctx, ds.reader(ctx), result, stmt, executionId); err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("SoftwareInstallerDetails").WithName(executionId), "get software installer details")
}
return nil, ctxerr.Wrap(ctx, err, "get software install details")
}
return result, nil
}
func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error) {
titleID, err := ds.getOrGenerateSoftwareInstallerTitleID(ctx, payload)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "get or generate software installer title ID")
}
if err := ds.addSoftwareTitleToMatchingSoftware(ctx, titleID, payload); err != nil {
return 0, ctxerr.Wrap(ctx, err, "add software title to matching software")
}
installScriptID, err := ds.getOrGenerateScriptContentsID(ctx, payload.InstallScript)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "get or generate install script contents ID")
}
uninstallScriptID, err := ds.getOrGenerateScriptContentsID(ctx, payload.UninstallScript)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "get or generate uninstall script contents ID")
}
var postInstallScriptID *uint
if payload.PostInstallScript != "" {
sid, err := ds.getOrGenerateScriptContentsID(ctx, payload.PostInstallScript)
if err != nil {
return 0, ctxerr.Wrap(ctx, err, "get or generate post-install script contents ID")
}
postInstallScriptID = &sid
}
var tid *uint
var globalOrTeamID uint
if payload.TeamID != nil {
globalOrTeamID = *payload.TeamID
if *payload.TeamID > 0 {
tid = payload.TeamID
}
}
stmt := `
INSERT INTO software_installers (
team_id,
global_or_team_id,
title_id,
storage_id,
filename,
extension,
version,
package_ids,
install_script_content_id,
pre_install_query,
post_install_script_content_id,
uninstall_script_content_id,
platform,
self_service,
user_id,
user_name,
user_email,
fleet_library_app_id
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?)`
args := []interface{}{
tid,
globalOrTeamID,
titleID,
payload.StorageID,
payload.Filename,
payload.Extension,
payload.Version,
strings.Join(payload.PackageIDs, ","),
installScriptID,
payload.PreInstallQuery,
postInstallScriptID,
uninstallScriptID,
payload.Platform,
payload.SelfService,
payload.UserID,
payload.UserID,
payload.UserID,
payload.FleetLibraryAppID,
}
res, err := ds.writer(ctx).ExecContext(ctx, stmt, args...)
if err != nil {
if IsDuplicate(err) {
// already exists for this team/no team
err = alreadyExists("SoftwareInstaller", payload.Title)
}
return 0, ctxerr.Wrap(ctx, err, "insert software installer")
}
id, _ := res.LastInsertId()
return uint(id), nil //nolint:gosec // dismiss G115
}
func (ds *Datastore) getOrGenerateSoftwareInstallerTitleID(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload) (uint, error) {
selectStmt := `SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''`
selectArgs := []any{payload.Title, payload.Source}
insertStmt := `INSERT INTO software_titles (name, source, browser) VALUES (?, ?, '')`
insertArgs := []any{payload.Title, payload.Source}
if payload.BundleIdentifier != "" {
// match by bundle identifier first, or standard matching if we don't have a bundle identifier match
selectStmt = `SELECT id FROM software_titles WHERE bundle_identifier = ? OR (name = ? AND source = ? AND browser = '') ORDER BY bundle_identifier = ? DESC LIMIT 1`
selectArgs = []any{payload.BundleIdentifier, payload.Title, payload.Source, payload.BundleIdentifier}
insertStmt = `INSERT INTO software_titles (name, source, bundle_identifier, browser) VALUES (?, ?, ?, '')`
insertArgs = append(insertArgs, payload.BundleIdentifier)
}
titleID, err := ds.optimisticGetOrInsert(ctx,
&parameterizedStmt{
Statement: selectStmt,
Args: selectArgs,
},
&parameterizedStmt{
Statement: insertStmt,
Args: insertArgs,
},
)
if err != nil {
return 0, err
}
return titleID, nil
}
func (ds *Datastore) addSoftwareTitleToMatchingSoftware(ctx context.Context, titleID uint, payload *fleet.UploadSoftwareInstallerPayload) error {
whereClause := "WHERE (s.name, s.source, s.browser) = (?, ?, '')"
whereArgs := []any{payload.Title, payload.Source}
if payload.BundleIdentifier != "" {
whereClause = "WHERE s.bundle_identifier = ?"
whereArgs = []any{payload.BundleIdentifier}
}
args := make([]any, 0, len(whereArgs))
args = append(args, titleID)
args = append(args, whereArgs...)
updateSoftwareStmt := fmt.Sprintf(`
UPDATE software s
SET s.title_id = ?
%s`, whereClause)
_, err := ds.writer(ctx).ExecContext(ctx, updateSoftwareStmt, args...)
return ctxerr.Wrap(ctx, err, "adding fk reference in software to software_titles")
}
func (ds *Datastore) UpdateInstallerSelfServiceFlag(ctx context.Context, selfService bool, id uint) error {
_, err := ds.writer(ctx).ExecContext(ctx, `UPDATE software_installers SET self_service = ? WHERE id = ?`, selfService, id)
if err != nil {
return ctxerr.Wrap(ctx, err, "update software installer")
}
return nil
}
func (ds *Datastore) SaveInstallerUpdates(ctx context.Context, payload *fleet.UpdateSoftwareInstallerPayload) error {
if payload.InstallScript == nil || payload.UninstallScript == nil || payload.PreInstallQuery == nil || payload.SelfService == nil {
return ctxerr.Wrap(ctx, errors.New("missing installer update payload fields"), "update installer record")
}
installScriptID, err := ds.getOrGenerateScriptContentsID(ctx, *payload.InstallScript)
if err != nil {
return ctxerr.Wrap(ctx, err, "get or generate install script contents ID")
}
uninstallScriptID, err := ds.getOrGenerateScriptContentsID(ctx, *payload.UninstallScript)
if err != nil {
return ctxerr.Wrap(ctx, err, "get or generate uninstall script contents ID")
}
var postInstallScriptID *uint
if payload.PostInstallScript != nil && *payload.PostInstallScript != "" { // pointer because optional
sid, err := ds.getOrGenerateScriptContentsID(ctx, *payload.PostInstallScript)
if err != nil {
return ctxerr.Wrap(ctx, err, "get or generate post-install script contents ID")
}
postInstallScriptID = &sid
}
touchUploaded := ""
if payload.InstallerFile != nil {
touchUploaded = ", uploaded_at = NOW()"
}
stmt := fmt.Sprintf(`UPDATE software_installers SET
storage_id = ?,
filename = ?,
version = ?,
package_ids = ?,
install_script_content_id = ?,
pre_install_query = ?,
post_install_script_content_id = ?,
uninstall_script_content_id = ?,
self_service = ?,
user_id = ?,
user_name = (SELECT name FROM users WHERE id = ?),
user_email = (SELECT email FROM users WHERE id = ?) %s
WHERE id = ?`, touchUploaded)
args := []interface{}{
payload.StorageID,
payload.Filename,
payload.Version,
strings.Join(payload.PackageIDs, ","),
installScriptID,
*payload.PreInstallQuery,
postInstallScriptID,
uninstallScriptID,
*payload.SelfService,
payload.UserID,
payload.UserID,
payload.UserID,
payload.InstallerID,
}
_, err = ds.writer(ctx).ExecContext(ctx, stmt, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "update software installer")
}
return nil
}
func (ds *Datastore) ValidateOrbitSoftwareInstallerAccess(ctx context.Context, hostID uint, installerID uint) (bool, error) {
query := `
SELECT 1
FROM
host_software_installs
WHERE
software_installer_id = ?
AND
host_id = ?
AND
install_script_exit_code IS NULL
`
var access bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &access, query, installerID, hostID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
return false, ctxerr.Wrap(ctx, err, "check software installer association to host")
}
return true, nil
}
func (ds *Datastore) GetSoftwareInstallerMetadataByID(ctx context.Context, id uint) (*fleet.SoftwareInstaller, error) {
query := `
SELECT
si.id,
si.team_id,
si.title_id,
si.storage_id,
si.package_ids,
si.filename,
si.extension,
si.version,
si.install_script_content_id,
si.pre_install_query,
si.post_install_script_content_id,
si.uninstall_script_content_id,
si.uploaded_at,
COALESCE(st.name, '') AS software_title,
si.platform,
si.fleet_library_app_id
FROM
software_installers si
LEFT OUTER JOIN software_titles st ON st.id = si.title_id
WHERE
si.id = ?`
var dest fleet.SoftwareInstaller
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, id)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("SoftwareInstaller").WithID(id), "get software installer metadata")
}
return nil, ctxerr.Wrap(ctx, err, "get software installer metadata")
}
return &dest, nil
}
func (ds *Datastore) GetSoftwareInstallerMetadataByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint, withScriptContents bool) (*fleet.SoftwareInstaller, error) {
var scriptContentsSelect, scriptContentsFrom string
if withScriptContents {
scriptContentsSelect = ` , inst.contents AS install_script, COALESCE(pinst.contents, '') AS post_install_script, uninst.contents AS uninstall_script `
scriptContentsFrom = ` LEFT OUTER JOIN script_contents inst ON inst.id = si.install_script_content_id
LEFT OUTER JOIN script_contents pinst ON pinst.id = si.post_install_script_content_id
LEFT OUTER JOIN script_contents uninst ON uninst.id = si.uninstall_script_content_id`
}
query := fmt.Sprintf(`
SELECT
si.id,
si.team_id,
si.title_id,
si.storage_id,
si.package_ids,
si.filename,
si.extension,
si.version,
si.install_script_content_id,
si.pre_install_query,
si.post_install_script_content_id,
si.uninstall_script_content_id,
si.uploaded_at,
si.self_service,
COALESCE(st.name, '') AS software_title
%s
FROM
software_installers si
JOIN software_titles st ON st.id = si.title_id
%s
WHERE
si.title_id = ? AND si.global_or_team_id = ?`,
scriptContentsSelect, scriptContentsFrom)
var tmID uint
if teamID != nil {
tmID = *teamID
}
var dest fleet.SoftwareInstaller
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, titleID, tmID)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("SoftwareInstaller"), "get software installer metadata")
}
return nil, ctxerr.Wrap(ctx, err, "get software installer metadata")
}
return &dest, nil
}
var (
errDeleteInstallerWithAssociatedPolicy = &fleet.ConflictError{Message: "Couldn't delete. Policy automation uses this software. Please disable policy automation for this software and try again."}
errDeleteInstallerInstalledDuringSetup = &fleet.ConflictError{Message: "Couldn't delete. This software is installed when new Macs boot. Please remove software in Controls > Setup experience and try again."}
)
func (ds *Datastore) DeleteSoftwareInstaller(ctx context.Context, id uint) error {
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
err := ds.runInstallerUpdateSideEffectsInTransaction(ctx, tx, id, true, true)
if err != nil {
return ctxerr.Wrap(ctx, err, "clean up related installs and uninstalls")
}
// allow delete only if install_during_setup is false
res, err := tx.ExecContext(ctx, `DELETE FROM software_installers WHERE id = ? AND install_during_setup = 0`, id)
if err != nil {
if isMySQLForeignKey(err) {
// Check if the software installer is referenced by a policy automation.
var count int
if err := sqlx.GetContext(ctx, tx, &count, `SELECT COUNT(*) FROM policies WHERE software_installer_id = ?`, id); err != nil {
return ctxerr.Wrapf(ctx, err, "getting reference from policies")
}
if count > 0 {
return errDeleteInstallerWithAssociatedPolicy
}
}
return ctxerr.Wrap(ctx, err, "delete software installer")
}
rows, _ := res.RowsAffected()
if rows == 0 {
// could be that the software installer does not exist, or it is installed
// during setup, do additional check.
var installDuringSetup bool
if err := sqlx.GetContext(ctx, tx, &installDuringSetup,
`SELECT install_during_setup FROM software_installers WHERE id = ?`, id); err != nil && !errors.Is(err, sql.ErrNoRows) {
return ctxerr.Wrap(ctx, err, "check if software installer is installed during setup")
}
if installDuringSetup {
return errDeleteInstallerInstalledDuringSetup
}
return notFound("SoftwareInstaller").WithID(id)
}
return nil
})
}
func (ds *Datastore) InsertSoftwareInstallRequest(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool, policyID *uint) (string, error) {
const (
getInstallerStmt = `SELECT filename, "version", title_id, COALESCE(st.name, '[deleted title]') title_name
FROM software_installers si LEFT JOIN software_titles st ON si.title_id = st.id WHERE si.id = ?`
insertStmt = `
INSERT INTO host_software_installs
(execution_id, host_id, software_installer_id, user_id, self_service, policy_id, installer_filename, version, software_title_id, software_title_name)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
hostExistsStmt = `SELECT 1 FROM hosts WHERE id = ?`
)
// we need to explicitly do this check here because we can't set a FK constraint on the schema
var hostExists bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &hostExists, hostExistsStmt, hostID)
if err != nil {
if err == sql.ErrNoRows {
return "", notFound("Host").WithID(hostID)
}
return "", ctxerr.Wrap(ctx, err, "checking if host exists")
}
var installerDetails struct {
Filename string `db:"filename"`
Version string `db:"version"`
TitleID *uint `db:"title_id"`
TitleName *string `db:"title_name"`
}
if err = sqlx.GetContext(ctx, ds.reader(ctx), &installerDetails, getInstallerStmt, softwareInstallerID); err != nil {
if err == sql.ErrNoRows {
return "", notFound("SoftwareInstaller").WithID(softwareInstallerID)
}
return "", ctxerr.Wrap(ctx, err, "getting installer data")
}
var userID *uint
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil {
userID = &ctxUser.ID
}
installID := uuid.NewString()
_, err = ds.writer(ctx).ExecContext(ctx, insertStmt,
installID,
hostID,
softwareInstallerID,
userID,
selfService,
policyID,
installerDetails.Filename,
installerDetails.Version,
installerDetails.TitleID,
installerDetails.TitleName,
)
return installID, ctxerr.Wrap(ctx, err, "inserting new install software request")
}
func (ds *Datastore) ProcessInstallerUpdateSideEffects(ctx context.Context, installerID uint, wasMetadataUpdated bool, wasPackageUpdated bool) error {
return ds.withTx(ctx, func(tx sqlx.ExtContext) error {
return ds.runInstallerUpdateSideEffectsInTransaction(ctx, tx, installerID, wasMetadataUpdated, wasPackageUpdated)
})
}
func (ds *Datastore) runInstallerUpdateSideEffectsInTransaction(ctx context.Context, tx sqlx.ExtContext, installerID uint, wasMetadataUpdated bool, wasPackageUpdated bool) error {
if wasMetadataUpdated || wasPackageUpdated { // cancel pending installs/uninstalls
// TODO make this less naive; this assumes that installs/uninstalls execute and report back immediately
_, err := tx.ExecContext(ctx, `DELETE FROM host_script_results WHERE execution_id IN (
SELECT execution_id FROM host_software_installs WHERE software_installer_id = ? AND status = 'pending_uninstall'
)`, installerID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete pending uninstall scripts")
}
_, err = tx.ExecContext(ctx, `DELETE FROM host_software_installs
WHERE software_installer_id = ? AND status IN('pending_install', 'pending_uninstall')`, installerID)
if err != nil {
return ctxerr.Wrap(ctx, err, "delete pending host software installs/uninstalls")
}
}
if wasPackageUpdated { // hide existing install counts
_, err := tx.ExecContext(ctx, `UPDATE host_software_installs SET removed = TRUE
WHERE software_installer_id = ? AND status IS NOT NULL AND host_deleted_at IS NULL`, installerID)
if err != nil {
return ctxerr.Wrap(ctx, err, "hide existing install counts")
}
}
return nil
}
func (ds *Datastore) InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
const (
getInstallerStmt = `SELECT title_id, COALESCE(st.name, '[deleted title]') title_name
FROM software_installers si LEFT JOIN software_titles st ON si.title_id = st.id WHERE si.id = ?`
insertStmt = `
INSERT INTO host_software_installs
(execution_id, host_id, software_installer_id, user_id, uninstall, installer_filename, software_title_id, software_title_name, version)
VALUES (?, ?, ?, ?, 1, '', ?, ?, 'unknown')
`
hostExistsStmt = `SELECT 1 FROM hosts WHERE id = ?`
)
// we need to explicitly do this check here because we can't set a FK constraint on the schema
var hostExists bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &hostExists, hostExistsStmt, hostID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return notFound("Host").WithID(hostID)
}
return ctxerr.Wrap(ctx, err, "checking if host exists")
}
var installerDetails struct {
TitleID *uint `db:"title_id"`
TitleName *string `db:"title_name"`
}
if err = sqlx.GetContext(ctx, ds.reader(ctx), &installerDetails, getInstallerStmt, softwareInstallerID); err != nil {
if err == sql.ErrNoRows {
return notFound("SoftwareInstaller").WithID(softwareInstallerID)
}
return ctxerr.Wrap(ctx, err, "getting installer data")
}
var userID *uint
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil {
userID = &ctxUser.ID
}
_, err = ds.writer(ctx).ExecContext(ctx, insertStmt,
executionID,
hostID,
softwareInstallerID,
userID,
installerDetails.TitleID,
installerDetails.TitleName,
)
return ctxerr.Wrap(ctx, err, "inserting new uninstall software request")
}
func (ds *Datastore) GetSoftwareInstallResults(ctx context.Context, resultsUUID string) (*fleet.HostSoftwareInstallerResult, error) {
query := `
SELECT
hsi.execution_id AS execution_id,
hsi.pre_install_query_output,
hsi.post_install_script_output,
hsi.install_script_output,
hsi.host_id AS host_id,
COALESCE(st.name, hsi.software_title_name) AS software_title,
hsi.software_title_id,
COALESCE(hsi.execution_status, '') AS status,
hsi.installer_filename AS software_package,
hsi.user_id AS user_id,
hsi.post_install_script_exit_code,
hsi.install_script_exit_code,
hsi.self_service,
hsi.host_deleted_at,
hsi.policy_id,
hsi.created_at as created_at,
hsi.updated_at as updated_at
FROM
host_software_installs hsi
LEFT JOIN software_titles st ON hsi.software_title_id = st.id
WHERE
hsi.execution_id = :execution_id
`
stmt, args, err := sqlx.Named(query, map[string]any{
"execution_id": resultsUUID,
})
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, stmt, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("HostSoftwareInstallerResult"), "get host software installer results")
}
return nil, ctxerr.Wrap(ctx, err, "get host software installer results")
}
return &dest, nil
}
func (ds *Datastore) GetSummaryHostSoftwareInstalls(ctx context.Context, installerID uint) (*fleet.SoftwareInstallerStatusSummary, error) {
var dest fleet.SoftwareInstallerStatusSummary
stmt := `
SELECT
COALESCE(SUM( IF(status = :software_status_pending_install, 1, 0)), 0) AS pending_install,
COALESCE(SUM( IF(status = :software_status_failed_install, 1, 0)), 0) AS failed_install,
COALESCE(SUM( IF(status = :software_status_pending_uninstall, 1, 0)), 0) AS pending_uninstall,
COALESCE(SUM( IF(status = :software_status_failed_uninstall, 1, 0)), 0) AS failed_uninstall,
COALESCE(SUM( IF(status = :software_status_installed, 1, 0)), 0) AS installed
FROM (
SELECT
software_installer_id,
status
FROM
host_software_installs hsi
WHERE
software_installer_id = :installer_id
AND id IN(
SELECT
max(id) -- ensure we use only the most recently created install attempt for each host
FROM host_software_installs
WHERE
software_installer_id = :installer_id
AND host_deleted_at IS NULL
AND removed = 0
GROUP BY
host_id)) s`
query, args, err := sqlx.Named(stmt, map[string]interface{}{
"installer_id": installerID,
"software_status_pending_install": fleet.SoftwareInstallPending,
"software_status_failed_install": fleet.SoftwareInstallFailed,
"software_status_pending_uninstall": fleet.SoftwareUninstallPending,
"software_status_failed_uninstall": fleet.SoftwareUninstallFailed,
"software_status_installed": fleet.SoftwareInstalled,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get summary host software installs: named query")
}
err = sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, args...)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get summary host software install status")
}
return &dest, nil
}
func (ds *Datastore) vppAppJoin(appID fleet.VPPAppID, status fleet.SoftwareInstallerStatus) (string, []interface{}, error) {
// Since VPP does not have uninstaller yet, we map the generic pending/failed statuses to the install statuses
switch status {
case fleet.SoftwarePending:
status = fleet.SoftwareInstallPending
case fleet.SoftwareFailed:
status = fleet.SoftwareInstallFailed
default:
// no change
}
stmt := fmt.Sprintf(`JOIN (
SELECT
host_id
FROM
host_vpp_software_installs hvsi
LEFT OUTER JOIN
nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid
WHERE
adam_id = :adam_id AND platform = :platform
AND hvsi.id IN(
SELECT
max(id) -- ensure we use only the most recent install attempt for each host
FROM host_vpp_software_installs
WHERE
adam_id = :adam_id AND platform = :platform
GROUP BY
host_id, adam_id)
AND (%s) = :status) hss ON hss.host_id = h.id
`, vppAppHostStatusNamedQuery("hvsi", "ncr", ""))
return sqlx.Named(stmt, map[string]interface{}{
"status": status,
"adam_id": appID.AdamID,
"platform": appID.Platform,
"software_status_installed": fleet.SoftwareInstalled,
"software_status_failed": fleet.SoftwareInstallFailed,
"software_status_pending": fleet.SoftwareInstallPending,
"mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged,
"mdm_status_error": fleet.MDMAppleStatusError,
"mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError,
})
}
func (ds *Datastore) softwareInstallerJoin(installerID uint, status fleet.SoftwareInstallerStatus) (string, []interface{}, error) {
statusFilter := "hsi.status = :status"
var status2 fleet.SoftwareInstallerStatus
switch status {
case fleet.SoftwarePending:
status = fleet.SoftwareInstallPending
status2 = fleet.SoftwareUninstallPending
case fleet.SoftwareFailed:
status = fleet.SoftwareInstallFailed
status2 = fleet.SoftwareUninstallFailed
default:
// no change
}
if status2 != "" {
statusFilter = "hsi.status IN (:status, :status2)"
}
stmt := fmt.Sprintf(`JOIN (
SELECT
host_id
FROM
host_software_installs hsi
WHERE
software_installer_id = :installer_id
AND hsi.id IN(
SELECT
max(id) -- ensure we use only the most recent install attempt for each host
FROM host_software_installs
WHERE
software_installer_id = :installer_id
AND removed = 0
GROUP BY
host_id, software_installer_id)
AND %s) hss ON hss.host_id = h.id
`, statusFilter)
return sqlx.Named(stmt, map[string]interface{}{
"status": status,
"status2": status2,
"installer_id": installerID,
})
}
func (ds *Datastore) GetHostLastInstallData(ctx context.Context, hostID, installerID uint) (*fleet.HostLastInstallData, error) {
stmt := `
SELECT execution_id, hsi.status
FROM host_software_installs hsi
WHERE hsi.id = (
SELECT
MAX(id)
FROM host_software_installs
WHERE
software_installer_id = :installer_id AND host_id = :host_id
GROUP BY
host_id, software_installer_id)
`
stmt, args, err := sqlx.Named(stmt, map[string]interface{}{
"host_id": hostID,
"installer_id": installerID,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "build named query to get host last install data")
}
var hostLastInstall fleet.HostLastInstallData
if err := sqlx.GetContext(ctx, ds.reader(ctx), &hostLastInstall, stmt, args...); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, ctxerr.Wrap(ctx, err, "get host last install data")
}
return &hostLastInstall, nil
}
func (ds *Datastore) CleanupUnusedSoftwareInstallers(ctx context.Context, softwareInstallStore fleet.SoftwareInstallerStore, removeCreatedBefore time.Time) error {
if softwareInstallStore == nil {
// no-op in this case, possible if not running with a Premium license
return nil
}
// get the list of software installers hashes that are in use
var storageIDs []string
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &storageIDs, `SELECT DISTINCT storage_id FROM software_installers`); err != nil {
return ctxerr.Wrap(ctx, err, "get list of software installers in use")
}
_, err := softwareInstallStore.Cleanup(ctx, storageIDs, removeCreatedBefore)
return ctxerr.Wrap(ctx, err, "cleanup unused software installers")
}
func (ds *Datastore) BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
const upsertSoftwareTitles = `
INSERT INTO software_titles
(name, source, browser)
VALUES
%s
ON DUPLICATE KEY UPDATE
name = VALUES(name),
source = VALUES(source),
browser = VALUES(browser)
`
const loadSoftwareTitles = `
SELECT
id
FROM
software_titles
WHERE (name, source, browser) IN (%s)
`
const unsetAllInstallersFromPolicies = `
UPDATE
policies
SET
software_installer_id = NULL
WHERE
team_id = ?
`
const deleteAllPendingUninstallScriptExecutions = `
DELETE FROM host_script_results WHERE execution_id IN (
SELECT execution_id FROM host_software_installs WHERE status = 'pending_uninstall'
AND software_installer_id IN (
SELECT id FROM software_installers WHERE global_or_team_id = ?
)
)
`
const deleteAllPendingSoftwareInstalls = `
DELETE FROM host_software_installs
WHERE status IN('pending_install', 'pending_uninstall')
AND software_installer_id IN (
SELECT id FROM software_installers WHERE global_or_team_id = ?
)
`
const markAllSoftwareInstallsAsRemoved = `
UPDATE host_software_installs SET removed = TRUE
WHERE status IS NOT NULL AND host_deleted_at IS NULL
AND software_installer_id IN (
SELECT id FROM software_installers WHERE global_or_team_id = ?
)
`
const deleteAllInstallersInTeam = `
DELETE FROM
software_installers
WHERE
global_or_team_id = ?
`
const deletePendingUninstallScriptExecutionsNotInList = `
DELETE FROM host_script_results WHERE execution_id IN (
SELECT execution_id FROM host_software_installs WHERE status = 'pending_uninstall'
AND software_installer_id IN (
SELECT id FROM software_installers WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
)
`
const deletePendingSoftwareInstallsNotInList = `
DELETE FROM host_software_installs
WHERE status IN('pending_install', 'pending_uninstall')
AND software_installer_id IN (
SELECT id FROM software_installers WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
`
const markSoftwareInstallsNotInListAsRemoved = `
UPDATE host_software_installs SET removed = TRUE
WHERE status IS NOT NULL AND host_deleted_at IS NULL
AND software_installer_id IN (
SELECT id FROM software_installers WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
`
const unsetInstallersNotInListFromPolicies = `
UPDATE
policies
SET
software_installer_id = NULL
WHERE
software_installer_id IN (
SELECT id FROM software_installers
WHERE global_or_team_id = ? AND
title_id NOT IN (?)
)
`
const countInstallDuringSetupNotInList = `
SELECT
COUNT(*)
FROM
software_installers
WHERE
global_or_team_id = ? AND
title_id NOT IN (?) AND
install_during_setup = 1
`
const deleteInstallersNotInList = `
DELETE FROM
software_installers
WHERE
global_or_team_id = ? AND
title_id NOT IN (?)
`
const checkExistingInstaller = `
SELECT id,
storage_id != ? is_package_modified,
install_script_content_id != ? OR uninstall_script_content_id != ? OR pre_install_query != ? OR
COALESCE(post_install_script_content_id != ? OR
(post_install_script_content_id IS NULL AND ? IS NOT NULL) OR
(? IS NULL AND post_install_script_content_id IS NOT NULL)
, FALSE) is_metadata_modified FROM software_installers
WHERE global_or_team_id = ? AND title_id IN (SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = '')
`
const insertNewOrEditedInstaller = `
INSERT INTO software_installers (
team_id,
global_or_team_id,
storage_id,
filename,
extension,
version,
install_script_content_id,
uninstall_script_content_id,
pre_install_query,
post_install_script_content_id,
platform,
self_service,
title_id,
user_id,
user_name,
user_email,
url,
package_ids,
install_during_setup
) VALUES (
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
(SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''),
?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?, ?, COALESCE(?, false)
)
ON DUPLICATE KEY UPDATE
install_script_content_id = VALUES(install_script_content_id),
uninstall_script_content_id = VALUES(uninstall_script_content_id),
post_install_script_content_id = VALUES(post_install_script_content_id),
storage_id = VALUES(storage_id),
filename = VALUES(filename),
extension = VALUES(extension),
version = VALUES(version),
pre_install_query = VALUES(pre_install_query),
platform = VALUES(platform),
self_service = VALUES(self_service),
user_id = VALUES(user_id),
user_name = VALUES(user_name),
user_email = VALUES(user_email),
url = VALUES(url),
install_during_setup = COALESCE(?, install_during_setup)
`
// use a team id of 0 if no-team
var globalOrTeamID uint
if tmID != nil {
globalOrTeamID = *tmID
}
// if we're batch-setting installers and replacing the ones installed during
// setup in the same go, no need to validate that we don't delete one marked
// as install during setup (since we're overwriting those). This is always
// called from fleetctl gitops, so it should always be the case anyway.
var replacingInstallDuringSetup bool
if len(installers) == 0 || installers[0].InstallDuringSetup != nil {
replacingInstallDuringSetup = true
}
if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// if no installers are provided, just delete whatever was in
// the table
if len(installers) == 0 {
if _, err := tx.ExecContext(ctx, unsetAllInstallersFromPolicies, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "unset all obsolete installers in policies")
}
if _, err := tx.ExecContext(ctx, deleteAllPendingUninstallScriptExecutions, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "delete all pending uninstall script executions")
}
if _, err := tx.ExecContext(ctx, deleteAllPendingSoftwareInstalls, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "delete all pending host software install records")
}
if _, err := tx.ExecContext(ctx, markAllSoftwareInstallsAsRemoved, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "mark all host software installs as removed")
}
if _, err := tx.ExecContext(ctx, deleteAllInstallersInTeam, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete software installers")
}
return nil
}
var args []any
for _, installer := range installers {
args = append(args, installer.Title, installer.Source, "")
}
values := strings.TrimSuffix(
strings.Repeat("(?,?,?),", len(installers)),
",",
)
if _, err := tx.ExecContext(ctx, fmt.Sprintf(upsertSoftwareTitles, values), args...); err != nil {
return ctxerr.Wrap(ctx, err, "insert new/edited software title")
}
var titleIDs []uint
if err := sqlx.SelectContext(ctx, tx, &titleIDs, fmt.Sprintf(loadSoftwareTitles, values), args...); err != nil {
return ctxerr.Wrap(ctx, err, "load existing titles")
}
stmt, args, err := sqlx.In(unsetInstallersNotInListFromPolicies, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to unset obsolete installers from policies")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "unset obsolete software installers from policies")
}
// check if any in the list are install_during_setup, fail if there is one
if !replacingInstallDuringSetup {
stmt, args, err = sqlx.In(countInstallDuringSetupNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to check installers install_during_setup")
}
var countInstallDuringSetup int
if err := sqlx.GetContext(ctx, tx, &countInstallDuringSetup, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "check installers installed during setup")
}
if countInstallDuringSetup > 0 {
return errDeleteInstallerInstalledDuringSetup
}
}
stmt, args, err = sqlx.In(deletePendingUninstallScriptExecutionsNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to delete pending uninstall script executions")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete pending uninstall script executions")
}
stmt, args, err = sqlx.In(deletePendingSoftwareInstallsNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to delete pending software installs")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete pending host software install records")
}
stmt, args, err = sqlx.In(markSoftwareInstallsNotInListAsRemoved, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to mark obsolete host software installs as removed")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "mark obsolete host software installs as removed")
}
stmt, args, err = sqlx.In(deleteInstallersNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to delete obsolete installers")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete software installers")
}
for _, installer := range installers {
isRes, err := insertScriptContents(ctx, tx, installer.InstallScript)
if err != nil {
return ctxerr.Wrapf(ctx, err, "inserting install script contents for software installer with name %q", installer.Filename)
}
installScriptID, _ := isRes.LastInsertId()
uisRes, err := insertScriptContents(ctx, tx, installer.UninstallScript)
if err != nil {
return ctxerr.Wrapf(ctx, err, "inserting uninstall script contents for software installer with name %q", installer.Filename)
}
uninstallScriptID, _ := uisRes.LastInsertId()
var postInstallScriptID *int64
if installer.PostInstallScript != "" {
pisRes, err := insertScriptContents(ctx, tx, installer.PostInstallScript)
if err != nil {
return ctxerr.Wrapf(ctx, err, "inserting post-install script contents for software installer with name %q", installer.Filename)
}
insertID, _ := pisRes.LastInsertId()
postInstallScriptID = &insertID
}
wasUpdatedArgs := []interface{}{
// package update
installer.StorageID,
// metadata update
installScriptID,
uninstallScriptID,
installer.PreInstallQuery,
postInstallScriptID,
postInstallScriptID,
postInstallScriptID,
// WHERE clause
globalOrTeamID,
installer.Title,
installer.Source,
}
// pull existing installer state if it exists so we can diff for side effects post-update
type existingInstallerUpdateCheckResult struct {
InstallerID uint `db:"id"`
IsPackageModified bool `db:"is_package_modified"`
IsMetadataModified bool `db:"is_metadata_modified"`
}
var existing []existingInstallerUpdateCheckResult
err = sqlx.SelectContext(ctx, tx, &existing, checkExistingInstaller, wasUpdatedArgs...)
if err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return ctxerr.Wrapf(ctx, err, "checking for existing installer with name %q", installer.Filename)
}
}
args := []interface{}{
tmID,
globalOrTeamID,
installer.StorageID,
installer.Filename,
installer.Extension,
installer.Version,
installScriptID,
uninstallScriptID,
installer.PreInstallQuery,
postInstallScriptID,
installer.Platform,
installer.SelfService,
installer.Title,
installer.Source,
installer.UserID,
installer.UserID,
installer.UserID,
installer.URL,
strings.Join(installer.PackageIDs, ","),
installer.InstallDuringSetup,
installer.InstallDuringSetup,
}
upsertQuery := insertNewOrEditedInstaller
if len(existing) > 0 && existing[0].IsPackageModified { // update uploaded_at for updated installer package
upsertQuery = fmt.Sprintf("%s, uploaded_at = NOW()", upsertQuery)
}
if _, err := tx.ExecContext(ctx, upsertQuery, args...); err != nil {
return ctxerr.Wrapf(ctx, err, "insert new/edited installer with name %q", installer.Filename)
}
// perform side effects if this was an update
if len(existing) > 0 {
if err := ds.runInstallerUpdateSideEffectsInTransaction(
ctx,
tx,
existing[0].InstallerID,
existing[0].IsMetadataModified,
existing[0].IsPackageModified,
); err != nil {
return ctxerr.Wrapf(ctx, err, "processing installer with name %q", installer.Filename)
}
}
}
return nil
}); err != nil {
return err
}
return nil
}
func (ds *Datastore) HasSelfServiceSoftwareInstallers(ctx context.Context, hostPlatform string, hostTeamID *uint) (bool, error) {
if fleet.IsLinux(hostPlatform) {
hostPlatform = "linux"
}
stmt := `SELECT 1
WHERE EXISTS (
SELECT 1
FROM software_installers
WHERE self_service = 1 AND platform = ? AND global_or_team_id = ?
) OR EXISTS (
SELECT 1
FROM vpp_apps_teams
WHERE self_service = 1 AND platform = ? AND global_or_team_id = ?
)`
var globalOrTeamID uint
if hostTeamID != nil {
globalOrTeamID = *hostTeamID
}
args := []interface{}{hostPlatform, globalOrTeamID, hostPlatform, globalOrTeamID}
var hasInstallers bool
err := sqlx.GetContext(ctx, ds.reader(ctx), &hasInstallers, stmt, args...)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return false, ctxerr.Wrap(ctx, err, "check for self-service software installers")
}
return hasInstallers, nil
}
func (ds *Datastore) GetSoftwareTitleNameFromExecutionID(ctx context.Context, executionID string) (string, error) {
stmt := `
SELECT name
FROM software_titles st
INNER JOIN software_installers si ON si.title_id = st.id
INNER JOIN host_software_installs hsi ON hsi.software_installer_id = si.id
WHERE hsi.execution_id = ?
`
var name string
err := sqlx.GetContext(ctx, ds.reader(ctx), &name, stmt, executionID)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "get software title name from execution ID")
}
return name, nil
}
func (ds *Datastore) GetSoftwareInstallersWithoutPackageIDs(ctx context.Context) (map[uint]string, error) {
query := `
SELECT id, storage_id FROM software_installers WHERE package_ids = ''
`
type result struct {
ID uint `db:"id"`
StorageID string `db:"storage_id"`
}
var results []result
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, query); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get software installers without package ID")
}
if len(results) == 0 {
return nil, nil
}
idMap := make(map[uint]string, len(results))
for _, r := range results {
idMap[r.ID] = r.StorageID
}
return idMap, nil
}
func (ds *Datastore) UpdateSoftwareInstallerWithoutPackageIDs(ctx context.Context, id uint,
payload fleet.UploadSoftwareInstallerPayload,
) error {
uninstallScriptID, err := ds.getOrGenerateScriptContentsID(ctx, payload.UninstallScript)
if err != nil {
return ctxerr.Wrap(ctx, err, "get or generate uninstall script contents ID")
}
query := `
UPDATE software_installers
SET package_ids = ?, uninstall_script_content_id = ?, extension = ?
WHERE id = ?
`
_, err = ds.writer(ctx).ExecContext(ctx, query, strings.Join(payload.PackageIDs, ","), uninstallScriptID, payload.Extension, id)
if err != nil {
return ctxerr.Wrap(ctx, err, "update software installer without package ID")
}
return nil
}
func (ds *Datastore) GetSoftwareInstallers(ctx context.Context, teamID uint) ([]fleet.SoftwarePackageResponse, error) {
const loadInsertedSoftwareInstallers = `
SELECT
team_id,
title_id,
url
FROM
software_installers
WHERE global_or_team_id = ?
`
var softwarePackages []fleet.SoftwarePackageResponse
// Using ds.writer(ctx) on purpose because this method is to be called after applying software.
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &softwarePackages, loadInsertedSoftwareInstallers, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get software installers")
}
return softwarePackages, nil
}