2024-05-02 21:00:06 +00:00
package mysql
import (
2024-05-07 20:50:44 +00:00
"bytes"
2024-05-02 21:00:06 +00:00
"context"
2025-02-11 19:53:11 +00:00
"database/sql"
2025-11-05 16:40:44 +00:00
"errors"
2024-12-17 19:28:17 +00:00
"fmt"
2024-05-07 20:50:44 +00:00
"os"
"path/filepath"
2025-05-27 20:08:08 +00:00
"sort"
2024-11-12 14:28:08 +00:00
"strings"
2024-05-02 21:00:06 +00:00
"testing"
2024-05-06 19:19:45 +00:00
"time"
2024-05-02 21:00:06 +00:00
2025-10-14 20:59:01 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxdb"
2024-05-07 20:50:44 +00:00
"github.com/fleetdm/fleet/v4/server/datastore/filesystem"
2025-02-18 21:28:54 +00:00
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
2025-10-14 20:59:01 +00:00
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql/testing_utils"
2024-05-02 21:00:06 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
2024-05-06 19:19:45 +00:00
"github.com/fleetdm/fleet/v4/server/test"
2024-05-02 21:00:06 +00:00
"github.com/google/uuid"
2024-09-07 13:07:22 +00:00
"github.com/jmoiron/sqlx"
2024-07-02 16:32:49 +00:00
"github.com/stretchr/testify/assert"
2024-05-02 21:00:06 +00:00
"github.com/stretchr/testify/require"
)
func TestSoftwareInstallers ( t * testing . T ) {
ds := CreateMySQLDS ( t )
cases := [ ] struct {
name string
fn func ( t * testing . T , ds * Datastore )
} {
2024-05-07 16:02:08 +00:00
{ "SoftwareInstallRequests" , testSoftwareInstallRequests } ,
2024-05-14 18:06:33 +00:00
{ "ListPendingSoftwareInstalls" , testListPendingSoftwareInstalls } ,
2024-05-06 19:09:25 +00:00
{ "GetSoftwareInstallResults" , testGetSoftwareInstallResult } ,
2024-05-07 20:50:44 +00:00
{ "CleanupUnusedSoftwareInstallers" , testCleanupUnusedSoftwareInstallers } ,
2024-05-14 18:06:33 +00:00
{ "BatchSetSoftwareInstallers" , testBatchSetSoftwareInstallers } ,
2024-05-10 18:44:49 +00:00
{ "GetSoftwareInstallerMetadataByTeamAndTitleID" , testGetSoftwareInstallerMetadataByTeamAndTitleID } ,
2024-07-02 16:32:49 +00:00
{ "HasSelfServiceSoftwareInstallers" , testHasSelfServiceSoftwareInstallers } ,
2024-10-16 12:26:23 +00:00
{ "DeleteSoftwareInstallers" , testDeleteSoftwareInstallers } ,
2024-12-16 17:47:34 +00:00
{ "testDeletePendingSoftwareInstallsForPolicy" , testDeletePendingSoftwareInstallsForPolicy } ,
2024-08-30 17:13:25 +00:00
{ "GetHostLastInstallData" , testGetHostLastInstallData } ,
2024-09-26 18:23:50 +00:00
{ "GetOrGenerateSoftwareInstallerTitleID" , testGetOrGenerateSoftwareInstallerTitleID } ,
2024-12-17 19:28:17 +00:00
{ "BatchSetSoftwareInstallersScopedViaLabels" , testBatchSetSoftwareInstallersScopedViaLabels } ,
2024-12-27 18:10:28 +00:00
{ "MatchOrCreateSoftwareInstallerWithAutomaticPolicies" , testMatchOrCreateSoftwareInstallerWithAutomaticPolicies } ,
2025-06-03 15:09:43 +00:00
{ "GetDetailsForUninstallFromExecutionID" , testGetDetailsForUninstallFromExecutionID } ,
2025-04-18 20:41:41 +00:00
{ "GetTeamsWithInstallerByHash" , testGetTeamsWithInstallerByHash } ,
2025-11-05 16:40:44 +00:00
{ "MatchOrCreateSoftwareInstallerDuplicateHash" , testMatchOrCreateSoftwareInstallerDuplicateHash } ,
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355)
Still adding tests but wanted to get this up for review of the overall
"shape" of the fix
When certain things happen like installer updates we delete pending
upcoming_activities(UA) and host_software_install(HSI) entries and need
to mark setup_experience_status_results(SESR) cancelled. When this
happens if that UA/HSI are being depended on by setup experience we need
to make sure that that setup experience result eventually gets marked
failed.
I kind of went back and forth a few times on how best to do this and
avoid race conditions. One thing I tried was looking at existence of the
UA/HSI but found that naively just trying to look at that in relation to
the SESR entry seemed to have a few race conditions that were hard to
resolve. There are a few possible states here we need to account for
such as:
un-activated, totally not yet running software install cancelled
activated but not yet running on the host software install cancelled
activated and running on the host software install cancelled before
results are completely reported back
What I eventually came around to was essentially that we want to mark
the SESR cancelled in the same transaction we delete the HSI/UA in. We
then finalize it by marking it failed and sending the activity the next
time the host fetches setupm experience results. The new cancelled
status never leaves fleet. This is a bit ugly but in my testing avoided
the race conditions and works well.
Note that to actually avoid setup experience hanging entirely we still
need to fix #29357 which encompasses several cases where the unified
queue can get completely stuck for a host
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [ ] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [ ] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
- [ ] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
{ "BatchSetSoftwareInstallersSetupExperienceSideEffects" , testBatchSetSoftwareInstallersSetupExperienceSideEffects } ,
2025-05-27 20:08:08 +00:00
{ "EditDeleteSoftwareInstallersActivateNextActivity" , testEditDeleteSoftwareInstallersActivateNextActivity } ,
{ "BatchSetSoftwareInstallersActivateNextActivity" , testBatchSetSoftwareInstallersActivateNextActivity } ,
2025-06-13 15:36:10 +00:00
{ "SaveInstallerUpdatesClearsFleetMaintainedAppID" , testSaveInstallerUpdatesClearsFleetMaintainedAppID } ,
2025-10-14 20:59:01 +00:00
{ "SoftwareInstallerReplicaLag" , testSoftwareInstallerReplicaLag } ,
2025-11-04 15:04:42 +00:00
{ "SoftwareTitleDisplayName" , testSoftwareTitleDisplayName } ,
2024-05-02 21:00:06 +00:00
}
for _ , c := range cases {
t . Run ( c . name , func ( t * testing . T ) {
defer TruncateTables ( t , ds )
c . fn ( t , ds )
} )
}
}
2024-05-14 18:06:33 +00:00
func testListPendingSoftwareInstalls ( t * testing . T , ds * Datastore ) {
2024-05-06 19:19:45 +00:00
ctx := context . Background ( )
2025-02-11 19:53:11 +00:00
t . Cleanup ( func ( ) { ds . testActivateSpecificNextActivities = nil } )
2024-05-06 19:19:45 +00:00
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
host2 := test . NewHost ( t , ds , "host2" , "2" , "host2key" , "host2uuid" , time . Now ( ) )
2025-01-21 20:26:00 +00:00
host3 := test . NewHost ( t , ds , "host3" , "3" , "host3key" , "host3uuid" , time . Now ( ) )
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2024-05-06 19:19:45 +00:00
2024-12-20 22:17:18 +00:00
err := ds . UpsertSecretVariables ( ctx , [ ] fleet . SecretVariable {
{
Name : "RUBBER" ,
Value : "DUCKY" ,
} ,
{
Name : "BIG" ,
Value : "BIRD" ,
} ,
{
Name : "COOKIE" ,
Value : "MONSTER" ,
} ,
} )
require . NoError ( t , err )
2024-11-12 14:28:08 +00:00
tfr1 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
2024-12-11 14:54:15 +00:00
installerID1 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-20 22:17:18 +00:00
InstallScript : "hello $FLEET_SECRET_RUBBER" ,
2024-05-15 12:40:06 +00:00
PreInstallQuery : "SELECT 1" ,
2024-12-20 22:17:18 +00:00
PostInstallScript : "world $FLEET_SECRET_BIG" ,
UninstallScript : "goodbye $FLEET_SECRET_COOKIE" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr1 ,
2024-05-15 12:40:06 +00:00
StorageID : "storage1" ,
Filename : "file1" ,
Title : "file1" ,
Version : "1.0" ,
Source : "apps" ,
2024-08-30 17:13:25 +00:00
UserID : user1 . ID ,
2024-12-17 00:17:13 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-15 12:40:06 +00:00
} )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2024-11-12 14:28:08 +00:00
tfr2 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
2024-12-11 14:54:15 +00:00
installerID2 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-05-15 12:40:06 +00:00
InstallScript : "world" ,
PreInstallQuery : "SELECT 2" ,
PostInstallScript : "hello" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr2 ,
2024-05-15 12:40:06 +00:00
StorageID : "storage2" ,
Filename : "file2" ,
Title : "file2" ,
Version : "2.0" ,
Source : "apps" ,
2024-08-30 17:13:25 +00:00
UserID : user1 . ID ,
2024-12-17 00:17:13 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-15 12:40:06 +00:00
} )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2024-11-12 14:28:08 +00:00
tfr3 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
2024-12-11 14:54:15 +00:00
installerID3 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-05-29 15:01:48 +00:00
InstallScript : "banana" ,
PreInstallQuery : "SELECT 3" ,
PostInstallScript : "apple" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr3 ,
2024-05-29 15:01:48 +00:00
StorageID : "storage3" ,
Filename : "file3" ,
Title : "file3" ,
Version : "3.0" ,
Source : "apps" ,
SelfService : true ,
2024-08-30 17:13:25 +00:00
UserID : user1 . ID ,
2024-12-17 00:17:13 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-29 15:01:48 +00:00
} )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
// ensure that nothing gets automatically activated, we want to control
// specific activation for this test
ds . testActivateSpecificNextActivities = [ ] string { "-" }
hostInstall1 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , installerID1 , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
time . Sleep ( time . Millisecond )
hostInstall2 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , installerID2 , fleet . HostSoftwareInstallOptions { } )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
time . Sleep ( time . Millisecond )
hostInstall3 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , installerID1 , fleet . HostSoftwareInstallOptions { } )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
time . Sleep ( time . Millisecond )
hostInstall4 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , installerID2 , fleet . HostSoftwareInstallOptions { } )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
pendingHost1 , err := ds . ListPendingSoftwareInstalls ( ctx , host1 . ID )
require . NoError ( t , err )
require . Equal ( t , 2 , len ( pendingHost1 ) )
require . Equal ( t , hostInstall1 , pendingHost1 [ 0 ] )
require . Equal ( t , hostInstall2 , pendingHost1 [ 1 ] )
pendingHost2 , err := ds . ListPendingSoftwareInstalls ( ctx , host2 . ID )
require . NoError ( t , err )
require . Equal ( t , 2 , len ( pendingHost2 ) )
require . Equal ( t , hostInstall3 , pendingHost2 [ 0 ] )
require . Equal ( t , hostInstall4 , pendingHost2 [ 1 ] )
// activate and set a result for hostInstall4 (installerID2)
ds . testActivateSpecificNextActivities = [ ] string { hostInstall4 }
_ , err = ds . activateNextUpcomingActivity ( ctx , ds . writer ( ctx ) , host2 . ID , "" )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-05-15 12:40:06 +00:00
HostID : host2 . ID ,
InstallUUID : hostInstall4 ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
// create a new pending install request on host2 for installerID2
hostInstall5 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , installerID2 , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
ds . testActivateSpecificNextActivities = [ ] string { hostInstall5 }
_ , err = ds . activateNextUpcomingActivity ( ctx , ds . writer ( ctx ) , host2 . ID , "" )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-05-15 12:40:06 +00:00
HostID : host2 . ID ,
InstallUUID : hostInstall5 ,
2024-09-07 13:07:22 +00:00
PreInstallConditionOutput : ptr . String ( "" ) , // pre-install query did not return results, so install failed
2024-05-15 12:40:06 +00:00
} )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
installDetailsList1 , err := ds . ListPendingSoftwareInstalls ( ctx , host1 . ID )
require . NoError ( t , err )
require . Equal ( t , 2 , len ( installDetailsList1 ) )
installDetailsList2 , err := ds . ListPendingSoftwareInstalls ( ctx , host2 . ID )
require . NoError ( t , err )
require . Equal ( t , 1 , len ( installDetailsList2 ) )
2024-05-15 12:40:06 +00:00
require . Contains ( t , installDetailsList1 , hostInstall1 )
require . Contains ( t , installDetailsList1 , hostInstall2 )
require . Contains ( t , installDetailsList2 , hostInstall3 )
2024-05-06 19:19:45 +00:00
2024-05-15 12:40:06 +00:00
exec1 , err := ds . GetSoftwareInstallDetails ( ctx , hostInstall1 )
2024-05-06 19:19:45 +00:00
require . NoError ( t , err )
require . Equal ( t , host1 . ID , exec1 . HostID )
2024-05-15 12:40:06 +00:00
require . Equal ( t , hostInstall1 , exec1 . ExecutionID )
2024-12-20 22:17:18 +00:00
require . Equal ( t , "hello DUCKY" , exec1 . InstallScript )
require . Equal ( t , "world BIRD" , exec1 . PostInstallScript )
2024-05-15 12:40:06 +00:00
require . Equal ( t , installerID1 , exec1 . InstallerID )
2024-05-06 19:19:45 +00:00
require . Equal ( t , "SELECT 1" , exec1 . PreInstallCondition )
2024-05-29 15:01:48 +00:00
require . False ( t , exec1 . SelfService )
2024-12-20 22:17:18 +00:00
assert . Equal ( t , "goodbye MONSTER" , exec1 . UninstallScript )
2025-09-16 17:26:14 +00:00
// Check that regular install has MaxRetries = 0
require . EqualValues ( t , 0 , exec1 . MaxRetries , "Regular install should have MaxRetries = 0" )
2024-05-29 15:01:48 +00:00
2025-02-11 19:53:11 +00:00
// add a self-service request for installerID3 on host1
hostInstall6 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , installerID3 , fleet . HostSoftwareInstallOptions { SelfService : true } )
require . NoError ( t , err )
ds . testActivateSpecificNextActivities = [ ] string { hostInstall6 }
_ , err = ds . activateNextUpcomingActivity ( ctx , ds . writer ( ctx ) , host1 . ID , "" )
2024-05-29 15:01:48 +00:00
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-05-29 15:01:48 +00:00
HostID : host1 . ID ,
InstallUUID : hostInstall6 ,
PreInstallConditionOutput : ptr . String ( "output" ) ,
} )
require . NoError ( t , err )
exec2 , err := ds . GetSoftwareInstallDetails ( ctx , hostInstall6 )
require . NoError ( t , err )
require . Equal ( t , host1 . ID , exec2 . HostID )
require . Equal ( t , hostInstall6 , exec2 . ExecutionID )
require . Equal ( t , "banana" , exec2 . InstallScript )
require . Equal ( t , "apple" , exec2 . PostInstallScript )
require . Equal ( t , installerID3 , exec2 . InstallerID )
require . Equal ( t , "SELECT 3" , exec2 . PreInstallCondition )
require . True ( t , exec2 . SelfService )
2025-01-21 20:26:00 +00:00
// Create install request, don't fulfil it, delete and restore host.
// Should not appear in list of pending installs for that host.
2025-02-11 19:53:11 +00:00
_ , err = ds . InsertSoftwareInstallRequest ( ctx , host3 . ID , installerID1 , fleet . HostSoftwareInstallOptions { } )
2025-01-21 20:26:00 +00:00
require . NoError ( t , err )
2025-08-03 06:18:13 +00:00
// Set LastEnrolledAt before deleting the host (simulating a DEP enrolled host)
host3 . LastEnrolledAt = time . Now ( )
2025-01-21 20:26:00 +00:00
err = ds . DeleteHost ( ctx , host3 . ID )
require . NoError ( t , err )
err = ds . RestoreMDMApplePendingDEPHost ( ctx , host3 )
require . NoError ( t , err )
hostInstalls4 , err := ds . ListPendingSoftwareInstalls ( ctx , host3 . ID )
require . NoError ( t , err )
require . Empty ( t , hostInstalls4 )
2025-09-16 17:26:14 +00:00
// Test MaxRetries for setup experience install
// Create a software install request that's part of setup experience
setupExperienceInstallID , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , installerID1 , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
// Insert a setup experience status result to simulate this install is part of setup experience
_ , err = ds . writer ( ctx ) . ExecContext ( ctx , `
2025-10-28 12:33:58 +00:00
INSERT INTO setup_experience_status_results
( host_uuid , name , status , software_installer_id , host_software_installs_execution_id )
2025-09-16 17:26:14 +00:00
VALUES ( ? , ? , ? , ? , ? ) ` ,
host1 . UUID , "test_software" , fleet . SetupExperienceStatusPending , installerID1 , setupExperienceInstallID )
require . NoError ( t , err )
// Get the install details and check MaxRetries = setupExperienceSoftwareInstallsRetries
setupExperienceInstallDetails , err := ds . GetSoftwareInstallDetails ( ctx , setupExperienceInstallID )
require . NoError ( t , err )
require . Equal ( t , host1 . ID , setupExperienceInstallDetails . HostID )
require . Equal ( t , setupExperienceInstallID , setupExperienceInstallDetails . ExecutionID )
require . Equal ( t , setupExperienceSoftwareInstallsRetries , setupExperienceInstallDetails . MaxRetries , "Setup experience install should have MaxRetries = %d" , setupExperienceSoftwareInstallsRetries )
2024-05-06 19:19:45 +00:00
}
2024-05-07 16:02:08 +00:00
func testSoftwareInstallRequests ( t * testing . T , ds * Datastore ) {
2024-05-02 21:00:06 +00:00
ctx := context . Background ( )
// create a team
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 2" } )
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2025-10-28 12:33:58 +00:00
createBuiltinLabels ( t , ds )
2025-12-30 03:28:45 +00:00
labelsByName , err := ds . LabelIDsByName ( ctx , [ ] string { fleet . BuiltinLabelNameAllHosts } , fleet . TeamFilter { } )
2025-10-28 12:33:58 +00:00
require . NoError ( t , err )
require . Len ( t , labelsByName , 1 )
2024-05-02 21:00:06 +00:00
cases := map [ string ] * uint {
"no team" : nil ,
"team" : & team . ID ,
}
for tc , teamID := range cases {
t . Run ( tc , func ( t * testing . T ) {
2024-05-07 16:02:08 +00:00
// non-existent installer
2024-05-15 12:40:06 +00:00
si , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , teamID , 1 , false )
2024-05-02 21:00:06 +00:00
var nfe fleet . NotFoundError
require . ErrorAs ( t , err , & nfe )
2024-05-07 16:02:08 +00:00
require . Nil ( t , si )
2024-05-02 21:00:06 +00:00
2024-12-11 14:54:15 +00:00
installerID , titleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
Title : "foo" ,
Source : "bar" ,
InstallScript : "echo" ,
TeamID : teamID ,
Filename : "foo.pkg" ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-02 21:00:06 +00:00
} )
require . NoError ( t , err )
2024-05-15 12:40:06 +00:00
installerMeta , err := ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
2024-05-02 21:00:06 +00:00
require . NoError ( t , err )
2024-12-11 14:54:15 +00:00
require . NotNil ( t , installerMeta . TitleID )
require . Equal ( t , titleID , * installerMeta . TitleID )
2024-05-15 12:40:06 +00:00
si , err = ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , teamID , * installerMeta . TitleID , false )
2024-05-07 16:02:08 +00:00
require . NoError ( t , err )
require . NotNil ( t , si )
require . Equal ( t , "foo.pkg" , si . Name )
2025-10-28 12:33:58 +00:00
inHouseID , inHouseTitleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
Title : "inhouse" ,
Source : "ios_apps" ,
TeamID : teamID ,
Filename : "inhouse.ipa" ,
Extension : "ipa" ,
Platform : "ios" ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
require . NotZero ( t , inHouseID )
require . NotZero ( t , inHouseTitleID )
2024-05-07 16:02:08 +00:00
// non-existent host
2025-02-11 19:53:11 +00:00
_ , err = ds . InsertSoftwareInstallRequest ( ctx , 12 , si . InstallerID , fleet . HostSoftwareInstallOptions { } )
2024-05-02 21:00:06 +00:00
require . ErrorAs ( t , err , & nfe )
2024-09-07 13:07:22 +00:00
// Host with software install pending
tag := "-pending_install"
hostPendingInstall , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-macos" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-macos" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
_ , err = ds . InsertSoftwareInstallRequest ( ctx , hostPendingInstall . ID , si . InstallerID , fleet . HostSoftwareInstallOptions { } )
2024-09-07 13:07:22 +00:00
require . NoError ( t , err )
2025-10-28 12:33:58 +00:00
// Host with in-house app install pending
tag = "-in-house-pending_install"
hostInHousePendingInstall , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "ios-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-ios" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-ios" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "ios" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
nanoEnroll ( t , ds , hostInHousePendingInstall , false )
err = ds . InsertHostInHouseAppInstall ( ctx , hostInHousePendingInstall . ID , inHouseID , inHouseTitleID , uuid . NewString ( ) , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
2024-09-07 13:07:22 +00:00
// Host with software install failed
tag = "-failed_install"
hostFailedInstall , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-macos" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-macos" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
execID , err := ds . InsertSoftwareInstallRequest ( ctx , hostFailedInstall . ID , si . InstallerID , fleet . HostSoftwareInstallOptions { } )
2024-09-07 13:07:22 +00:00
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : hostFailedInstall . ID ,
InstallUUID : execID ,
InstallScriptExitCode : ptr . Int ( 1 ) ,
2024-09-07 13:07:22 +00:00
} )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
2024-09-07 13:07:22 +00:00
2025-10-28 12:33:58 +00:00
// Host with in-house app failed install
tag = "-in-house-failed_install"
hostInHouseFailedInstall , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "ios-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-ios" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-ios" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "ios" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
nanoEnroll ( t , ds , hostInHouseFailedInstall , false )
cmdUUID := uuid . NewString ( )
err = ds . InsertHostInHouseAppInstall ( ctx , hostInHouseFailedInstall . ID , inHouseID , inHouseTitleID , cmdUUID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
// record a failed verification for that in-house app install
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , `
INSERT INTO nano_command_results ( id , command_uuid , status , result )
VALUES ( ? , ? , ' Error ' , ' < ? xml version = "1.0" ? > < plist > < / plist > ' ) ` ,
hostInHouseFailedInstall . UUID , cmdUUID )
if err != nil {
return err
}
_ , err = q . ExecContext ( ctx , `
UPDATE host_in_house_software_installs
SET verification_command_uuid = ? , verification_failed_at = NOW ( 6 )
WHERE command_uuid = ? AND host_id = ? ` ,
uuid . NewString ( ) , cmdUUID , hostInHouseFailedInstall . ID ,
)
return err
} )
_ , err = ds . activateNextUpcomingActivity ( ctx , ds . writer ( ctx ) , hostInHouseFailedInstall . ID , cmdUUID )
require . NoError ( t , err )
2024-09-07 13:07:22 +00:00
// Host with software install successful
tag = "-installed"
hostInstalled , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-macos" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-macos" + tag + tc ) ,
2024-05-02 21:00:06 +00:00
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
execID , err = ds . InsertSoftwareInstallRequest ( ctx , hostInstalled . ID , si . InstallerID , fleet . HostSoftwareInstallOptions { } )
2024-05-02 21:00:06 +00:00
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : hostInstalled . ID ,
InstallUUID : execID ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
2024-09-07 13:07:22 +00:00
} )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
2024-09-07 13:07:22 +00:00
2025-10-28 12:33:58 +00:00
// host with in-house successful install
tag = "-in-house-installed"
hostInHouseInstalled , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "ios-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-ios" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-ios" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "ios" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
nanoEnroll ( t , ds , hostInHouseInstalled , false )
cmdUUID = uuid . NewString ( )
err = ds . InsertHostInHouseAppInstall ( ctx , hostInHouseInstalled . ID , inHouseID , inHouseTitleID , cmdUUID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
// record a successful verification for that in-house app install
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , `
INSERT INTO nano_command_results ( id , command_uuid , status , result )
VALUES ( ? , ? , ' Acknowledged ' , ' < ? xml version = "1.0" ? > < plist > < / plist > ' ) ` ,
hostInHouseInstalled . UUID , cmdUUID )
if err != nil {
return err
}
_ , err = q . ExecContext ( ctx , `
UPDATE host_in_house_software_installs
SET verification_command_uuid = ? , verification_at = NOW ( 6 )
WHERE command_uuid = ? AND host_id = ? ` ,
uuid . NewString ( ) , cmdUUID , hostInHouseInstalled . ID ,
)
return err
} )
_ , err = ds . activateNextUpcomingActivity ( ctx , ds . writer ( ctx ) , hostInHouseInstalled . ID , cmdUUID )
require . NoError ( t , err )
2024-09-07 13:07:22 +00:00
// Host with pending uninstall
tag = "-pending_uninstall"
hostPendingUninstall , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-macos" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-macos" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
2025-06-03 15:09:43 +00:00
err = ds . InsertSoftwareUninstallRequest ( ctx , "uuid" + tag + tc , hostPendingUninstall . ID , si . InstallerID , false )
2024-09-07 13:07:22 +00:00
require . NoError ( t , err )
// Host with failed uninstall
tag = "-failed_uninstall"
hostFailedUninstall , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-macos" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-macos" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
execID = "uuid" + tag + tc
2025-06-03 15:09:43 +00:00
err = ds . InsertSoftwareUninstallRequest ( ctx , execID , hostFailedUninstall . ID , si . InstallerID , false )
2024-09-07 13:07:22 +00:00
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
_ , _ , err = ds . SetHostScriptExecutionResult ( ctx , & fleet . HostScriptResultPayload {
HostID : hostFailedUninstall . ID ,
ExecutionID : execID ,
ExitCode : 1 ,
2024-09-07 13:07:22 +00:00
} )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
2024-09-07 13:07:22 +00:00
// Host with successful uninstall
tag = "-uninstalled"
hostUninstalled , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test" + tag + tc ,
OsqueryHostID : ptr . String ( "osquery-macos" + tag + tc ) ,
NodeKey : ptr . String ( "node-key-macos" + tag + tc ) ,
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : teamID ,
} )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
execID = "uuid" + tag + tc
2025-06-03 15:09:43 +00:00
err = ds . InsertSoftwareUninstallRequest ( ctx , execID , hostUninstalled . ID , si . InstallerID , false )
2024-09-07 13:07:22 +00:00
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
_ , _ , err = ds . SetHostScriptExecutionResult ( ctx , & fleet . HostScriptResultPayload {
HostID : hostUninstalled . ID ,
ExecutionID : execID ,
ExitCode : 0 ,
2024-09-07 13:07:22 +00:00
} )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
2024-05-08 20:52:35 +00:00
2024-09-09 19:43:52 +00:00
// Uninstall request with unknown host
2025-06-03 15:09:43 +00:00
err = ds . InsertSoftwareUninstallRequest ( ctx , "uuid" + tag + tc , 99999 , si . InstallerID , false )
2024-09-09 19:43:52 +00:00
assert . ErrorContains ( t , err , "Host" )
2025-10-28 12:33:58 +00:00
allHostIDs := [ ] uint {
hostPendingInstall . ID ,
hostFailedInstall . ID ,
hostInstalled . ID ,
hostPendingUninstall . ID ,
hostFailedUninstall . ID ,
hostUninstalled . ID ,
hostInHousePendingInstall . ID ,
hostInHouseFailedInstall . ID ,
hostInHouseInstalled . ID ,
}
for _ , hid := range allHostIDs {
err = ds . AddLabelsToHost ( ctx , hid , [ ] uint { labelsByName [ fleet . BuiltinLabelNameAllHosts ] } )
require . NoError ( t , err )
}
2024-05-08 20:52:35 +00:00
userTeamFilter := fleet . TeamFilter {
User : & fleet . User { GlobalRole : ptr . String ( "admin" ) } ,
}
2024-09-07 13:07:22 +00:00
2025-02-11 19:53:11 +00:00
// for this test, teamID is nil for no-team, but the ListHosts filter
// returns "all teams" if TeamFilter = nil, it needs to use TeamFilter =
// 0 for "no team" only.
teamFilter := teamID
if teamFilter == nil {
teamFilter = ptr . Uint ( 0 )
}
// get the names of hosts, useful for debugging
getHostNames := func ( hosts [ ] * fleet . Host ) [ ] string {
2025-10-28 12:33:58 +00:00
hostNames := make ( [ ] string , 0 , len ( hosts ) )
2025-02-11 19:53:11 +00:00
for _ , h := range hosts {
hostNames = append ( hostNames , h . Hostname )
}
return hostNames
}
2025-10-28 12:33:58 +00:00
pluckHostIDs := func ( hosts [ ] * fleet . Host ) [ ] uint {
hostIDs := make ( [ ] uint , 0 , len ( hosts ) )
for _ , h := range hosts {
hostIDs = append ( hostIDs , h . ID )
}
return hostIDs
}
cases := [ ] struct {
desc string
opts fleet . HostListOptions
wantHostIDs [ ] uint
} {
{
desc : "list hosts with software install pending requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareInstallPending ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostPendingInstall . ID } ,
} ,
{
desc : "list hosts with in-house app pending install" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : & inHouseTitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareInstallPending ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostInHousePendingInstall . ID } ,
} ,
{
desc : "list hosts with all pending requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwarePending ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostPendingInstall . ID , hostPendingUninstall . ID } ,
} ,
{
desc : "list hosts with in-house app all pending requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : & inHouseTitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwarePending ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostInHousePendingInstall . ID } ,
} ,
{
desc : "list hosts with software install failed requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareInstallFailed ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostFailedInstall . ID } ,
} ,
{
desc : "list hosts with in-house install failed requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : & inHouseTitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareInstallFailed ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostInHouseFailedInstall . ID } ,
} ,
{
desc : "list hosts with all failed requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareFailed ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostFailedInstall . ID , hostFailedUninstall . ID } ,
} ,
{
desc : "list hosts with in-house all failed requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : & inHouseTitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareFailed ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostInHouseFailedInstall . ID } ,
} ,
{
desc : "list hosts with software installed" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareInstalled ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostInstalled . ID } ,
} ,
{
desc : "list hosts with in-house app installed" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : & inHouseTitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareInstalled ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostInHouseInstalled . ID } ,
} ,
{
desc : "list hosts with pending software uninstall requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareUninstallPending ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostPendingUninstall . ID } ,
} ,
{
desc : "list hosts with failed software uninstall requests" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
SoftwareStatusFilter : ptr . T ( fleet . SoftwareUninstallFailed ) ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { hostFailedUninstall . ID } ,
} ,
{
desc : "list all hosts with the software title" ,
opts : fleet . HostListOptions {
ListOptions : fleet . ListOptions { PerPage : 100 } ,
SoftwareTitleIDFilter : installerMeta . TitleID ,
TeamFilter : teamFilter ,
} ,
wantHostIDs : [ ] uint { } ,
} ,
}
for _ , c := range cases {
t . Run ( c . desc , func ( t * testing . T ) {
hosts , err := ds . ListHosts ( ctx , userTeamFilter , c . opts )
require . NoError ( t , err )
require . Len ( t , hosts , len ( c . wantHostIDs ) , getHostNames ( hosts ) )
require . ElementsMatch ( t , c . wantHostIDs , pluckHostIDs ( hosts ) )
if c . opts . SoftwareStatusFilter == nil && c . opts . SoftwareTitleIDFilter != nil {
// for list hosts by label, if no status is provided, the title ID filter is ignored/no-op,
// so all host IDs are returned
c . wantHostIDs = allHostIDs
}
hosts , err = ds . ListHostsInLabel ( ctx , userTeamFilter , labelsByName [ fleet . BuiltinLabelNameAllHosts ] , c . opts )
require . NoError ( t , err )
require . Len ( t , hosts , len ( c . wantHostIDs ) , getHostNames ( hosts ) )
require . ElementsMatch ( t , c . wantHostIDs , pluckHostIDs ( hosts ) )
} )
}
2024-05-08 20:52:35 +00:00
summary , err := ds . GetSummaryHostSoftwareInstalls ( ctx , installerMeta . InstallerID )
require . NoError ( t , err )
require . Equal ( t , fleet . SoftwareInstallerStatusSummary {
2024-09-09 19:43:52 +00:00
Installed : 1 ,
PendingInstall : 1 ,
FailedInstall : 1 ,
PendingUninstall : 1 ,
FailedUninstall : 1 ,
2024-05-08 20:52:35 +00:00
} , * summary )
2025-10-28 12:33:58 +00:00
vppSummary , err := ds . GetSummaryHostInHouseAppInstalls ( ctx , teamID , inHouseID )
require . NoError ( t , err )
require . Equal ( t , fleet . VPPAppStatusSummary {
Installed : 1 ,
Pending : 1 ,
Failed : 1 ,
} , * vppSummary )
2024-05-02 21:00:06 +00:00
} )
}
}
2024-05-06 19:09:25 +00:00
func testGetSoftwareInstallResult ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 2" } )
require . NoError ( t , err )
teamID := team . ID
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2024-05-06 19:09:25 +00:00
for _ , tc := range [ ] struct {
name string
expectedStatus fleet . SoftwareInstallerStatus
2024-05-15 12:40:06 +00:00
postInstallScriptEC * int
2024-05-06 19:09:25 +00:00
preInstallQueryOutput * string
2024-05-15 12:40:06 +00:00
installScriptEC * int
2024-05-06 19:09:25 +00:00
postInstallScriptOutput * string
installScriptOutput * string
} {
{
name : "pending install" ,
2024-09-04 21:46:48 +00:00
expectedStatus : fleet . SoftwareInstallPending ,
2024-05-06 19:09:25 +00:00
postInstallScriptOutput : ptr . String ( "post install output" ) ,
installScriptOutput : ptr . String ( "install output" ) ,
} ,
{
name : "failing install post install script" ,
2024-09-04 21:46:48 +00:00
expectedStatus : fleet . SoftwareInstallFailed ,
2024-05-15 12:40:06 +00:00
postInstallScriptEC : ptr . Int ( 1 ) ,
2024-05-06 19:09:25 +00:00
postInstallScriptOutput : ptr . String ( "post install output" ) ,
installScriptOutput : ptr . String ( "install output" ) ,
} ,
{
name : "failing install install script" ,
2024-09-04 21:46:48 +00:00
expectedStatus : fleet . SoftwareInstallFailed ,
2024-05-15 12:40:06 +00:00
installScriptEC : ptr . Int ( 1 ) ,
2024-05-06 19:09:25 +00:00
postInstallScriptOutput : ptr . String ( "post install output" ) ,
installScriptOutput : ptr . String ( "install output" ) ,
} ,
{
name : "failing install pre install query" ,
2024-09-04 21:46:48 +00:00
expectedStatus : fleet . SoftwareInstallFailed ,
2024-05-06 19:09:25 +00:00
preInstallQueryOutput : ptr . String ( "" ) ,
postInstallScriptOutput : ptr . String ( "post install output" ) ,
installScriptOutput : ptr . String ( "install output" ) ,
} ,
} {
t . Run ( tc . name , func ( t * testing . T ) {
// create a host and software installer
swFilename := "file_" + tc . name + ".pkg"
2024-12-11 14:54:15 +00:00
installerID , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
Title : "foo" + tc . name ,
Source : "bar" + tc . name ,
InstallScript : "echo " + tc . name ,
Version : "1.11" ,
TeamID : & teamID ,
Filename : swFilename ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-06 19:09:25 +00:00
} )
require . NoError ( t , err )
host , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "macos-test-" + tc . name ,
ComputerName : "macos-test-" + tc . name ,
OsqueryHostID : ptr . String ( "osquery-macos-" + tc . name ) ,
NodeKey : ptr . String ( "node-key-macos-" + tc . name ) ,
UUID : uuid . NewString ( ) ,
Platform : "darwin" ,
TeamID : & teamID ,
} )
require . NoError ( t , err )
2024-10-01 16:02:13 +00:00
beforeInstallRequest := time . Now ( )
2025-02-11 19:53:11 +00:00
installUUID , err := ds . InsertSoftwareInstallRequest ( ctx , host . ID , installerID , fleet . HostSoftwareInstallOptions { } )
2024-05-15 12:40:06 +00:00
require . NoError ( t , err )
2024-10-01 16:02:13 +00:00
res , err := ds . GetSoftwareInstallResults ( ctx , installUUID )
require . NoError ( t , err )
require . NotNil ( t , res . UpdatedAt )
require . Less ( t , beforeInstallRequest , res . CreatedAt )
createdAt := res . CreatedAt
require . Less ( t , beforeInstallRequest , * res . UpdatedAt )
beforeInstallResult := time . Now ( )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-05-15 12:40:06 +00:00
HostID : host . ID ,
InstallUUID : installUUID ,
PreInstallConditionOutput : tc . preInstallQueryOutput ,
InstallScriptExitCode : tc . installScriptEC ,
InstallScriptOutput : tc . installScriptOutput ,
PostInstallScriptExitCode : tc . postInstallScriptEC ,
PostInstallScriptOutput : tc . postInstallScriptOutput ,
} )
2024-05-06 19:09:25 +00:00
require . NoError ( t , err )
2024-10-21 22:46:50 +00:00
// edit installer to ensure host software install is unaffected
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err = q . ExecContext ( ctx , `
UPDATE software_installers SET filename = ' something different ' , version = ' 1.23 ' WHERE id = ? ` ,
installerID )
require . NoError ( t , err )
return nil
} )
res , err = ds . GetSoftwareInstallResults ( ctx , installUUID )
require . NoError ( t , err )
require . Equal ( t , swFilename , res . SoftwarePackage )
2024-10-31 23:04:06 +00:00
// delete installer to confirm that we can still access the install record (unless pending)
2024-10-21 22:46:50 +00:00
err = ds . DeleteSoftwareInstaller ( ctx , installerID )
require . NoError ( t , err )
2024-10-31 23:04:06 +00:00
if tc . expectedStatus == fleet . SoftwareInstallPending { // expect pending to be deleted
_ , err = ds . GetSoftwareInstallResults ( ctx , installUUID )
require . Error ( t , err , notFound ( "HostSoftwareInstallerResult" ) )
return
}
2024-10-22 17:47:46 +00:00
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
// ensure version is not changed, though we don't expose it yet
2024-10-21 22:46:50 +00:00
var version string
err := sqlx . GetContext ( ctx , q , & version , ` SELECT "version" FROM host_software_installs WHERE execution_id = ? ` , installUUID )
require . NoError ( t , err )
require . Equal ( t , "1.11" , version )
return nil
} )
2024-10-22 17:47:46 +00:00
2024-10-01 16:02:13 +00:00
res , err = ds . GetSoftwareInstallResults ( ctx , installUUID )
2024-05-06 19:09:25 +00:00
require . NoError ( t , err )
2024-05-15 12:40:06 +00:00
require . Equal ( t , installUUID , res . InstallUUID )
2024-05-06 19:09:25 +00:00
require . Equal ( t , tc . expectedStatus , res . Status )
require . Equal ( t , swFilename , res . SoftwarePackage )
require . Equal ( t , host . ID , res . HostID )
2024-05-15 22:18:35 +00:00
require . Equal ( t , tc . preInstallQueryOutput , res . PreInstallQueryOutput )
require . Equal ( t , tc . postInstallScriptOutput , res . PostInstallScriptOutput )
require . Equal ( t , tc . installScriptOutput , res . Output )
2024-10-01 16:02:13 +00:00
require . NotNil ( t , res . CreatedAt )
require . Equal ( t , createdAt , res . CreatedAt )
require . NotNil ( t , res . UpdatedAt )
require . Less ( t , beforeInstallResult , * res . UpdatedAt )
2024-05-06 19:09:25 +00:00
} )
}
}
2024-05-07 20:50:44 +00:00
func testCleanupUnusedSoftwareInstallers ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
dir := t . TempDir ( )
store , err := filesystem . NewSoftwareInstallerStore ( dir )
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2024-05-07 20:50:44 +00:00
assertExisting := func ( want [ ] string ) {
dirEnts , err := os . ReadDir ( filepath . Join ( dir , "software-installers" ) )
require . NoError ( t , err )
got := make ( [ ] string , 0 , len ( dirEnts ) )
for _ , de := range dirEnts {
if de . Type ( ) . IsRegular ( ) {
got = append ( got , de . Name ( ) )
}
}
require . ElementsMatch ( t , want , got )
}
// cleanup an empty store
2024-08-13 12:27:10 +00:00
err = ds . CleanupUnusedSoftwareInstallers ( ctx , store , time . Now ( ) )
2024-05-07 20:50:44 +00:00
require . NoError ( t , err )
assertExisting ( nil )
// put an installer and save it in the DB
ins0 := "installer0"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
err = store . Put ( ctx , ins0 , ins0File )
require . NoError ( t , err )
2024-11-12 14:28:08 +00:00
_ , _ = ins0File . Seek ( 0 , 0 )
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
2024-05-07 20:50:44 +00:00
assertExisting ( [ ] string { ins0 } )
2024-12-11 14:54:15 +00:00
swi , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer0" ,
Title : "ins0" ,
Source : "apps" ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-07 20:50:44 +00:00
} )
require . NoError ( t , err )
assertExisting ( [ ] string { ins0 } )
2024-08-13 12:27:10 +00:00
err = ds . CleanupUnusedSoftwareInstallers ( ctx , store , time . Now ( ) )
2024-05-07 20:50:44 +00:00
require . NoError ( t , err )
assertExisting ( [ ] string { ins0 } )
// remove it from the DB, will now cleanup
err = ds . DeleteSoftwareInstaller ( ctx , swi )
require . NoError ( t , err )
2024-08-13 12:27:10 +00:00
// would clean up, but not created before 1m ago
err = ds . CleanupUnusedSoftwareInstallers ( ctx , store , time . Now ( ) . Add ( - time . Minute ) )
require . NoError ( t , err )
assertExisting ( [ ] string { ins0 } )
// do actual cleanup
err = ds . CleanupUnusedSoftwareInstallers ( ctx , store , time . Now ( ) . Add ( time . Minute ) )
2024-05-07 20:50:44 +00:00
require . NoError ( t , err )
assertExisting ( nil )
}
2024-05-10 18:44:49 +00:00
2024-05-14 18:06:33 +00:00
func testBatchSetSoftwareInstallers ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
2025-02-11 19:53:11 +00:00
t . Cleanup ( func ( ) { ds . testActivateSpecificNextActivities = nil } )
2024-05-14 18:06:33 +00:00
// create a team
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : t . Name ( ) } )
require . NoError ( t , err )
2025-02-11 19:53:11 +00:00
// create a couple hosts
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
host2 := test . NewHost ( t , ds , "host2" , "2" , "host2key" , "host2uuid" , time . Now ( ) )
2025-07-17 14:20:49 +00:00
err = ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( & team . ID , [ ] uint { host1 . ID , host2 . ID } ) )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2024-10-04 01:03:40 +00:00
// TODO(roberto): perform better assertions, we should have everything
2024-05-14 18:06:33 +00:00
// to check that the actual values of everything match.
assertSoftware := func ( wantTitles [ ] fleet . SoftwareTitle ) {
tmFilter := fleet . TeamFilter { User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } }
titles , _ , _ , err := ds . ListSoftwareTitles (
ctx ,
fleet . SoftwareTitleListOptions { TeamID : & team . ID } ,
tmFilter ,
)
require . NoError ( t , err )
require . Len ( t , titles , len ( wantTitles ) )
for _ , title := range titles {
2024-05-15 12:40:06 +00:00
meta , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , title . ID , false )
2024-05-14 18:06:33 +00:00
require . NoError ( t , err )
require . NotNil ( t , meta . TitleID )
}
}
// batch set with everything empty
2024-09-20 14:55:47 +00:00
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , nil )
require . NoError ( t , err )
softwareInstallers , err := ds . GetSoftwareInstallers ( ctx , team . ID )
2024-05-14 18:06:33 +00:00
require . NoError ( t , err )
2024-09-06 22:10:28 +00:00
require . Empty ( t , softwareInstallers )
2024-05-14 18:06:33 +00:00
assertSoftware ( nil )
2024-09-20 14:55:47 +00:00
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { } )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
2024-05-14 18:06:33 +00:00
require . NoError ( t , err )
2024-09-06 22:10:28 +00:00
require . Empty ( t , softwareInstallers )
2024-05-14 18:06:33 +00:00
assertSoftware ( nil )
// add a single installer
ins0 := "installer0"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
2024-11-12 14:28:08 +00:00
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
2025-12-08 15:01:07 +00:00
displayName := "Display name 1"
2025-05-07 23:16:08 +00:00
maintainedApp , err := ds . UpsertMaintainedApp ( ctx , & fleet . MaintainedApp {
Name : "Maintained1" ,
Slug : "maintained1" ,
2025-02-11 18:23:20 +00:00
Platform : "darwin" ,
2025-05-07 23:16:08 +00:00
UniqueIdentifier : "fleet.maintained1" ,
} )
require . NoError ( t , err )
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { {
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer0" ,
Title : "ins0" ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "foo" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
BundleIdentifier : "com.example.ins0" ,
2025-12-08 15:01:07 +00:00
DisplayName : displayName ,
2025-05-07 23:16:08 +00:00
FleetMaintainedAppID : ptr . Uint ( maintainedApp . ID ) ,
2024-05-14 18:06:33 +00:00
} } )
require . NoError ( t , err )
2024-09-20 14:55:47 +00:00
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
2024-09-06 22:10:28 +00:00
require . Len ( t , softwareInstallers , 1 )
2024-09-17 16:30:27 +00:00
require . NotNil ( t , softwareInstallers [ 0 ] . TeamID )
require . Equal ( t , team . ID , * softwareInstallers [ 0 ] . TeamID )
2024-09-06 22:10:28 +00:00
require . NotNil ( t , softwareInstallers [ 0 ] . TitleID )
2024-09-17 16:30:27 +00:00
require . Equal ( t , "https://example.com" , softwareInstallers [ 0 ] . URL )
2025-05-07 23:16:08 +00:00
require . Equal ( t , maintainedApp . ID , * softwareInstallers [ 0 ] . FleetMaintainedAppID )
2024-05-14 18:06:33 +00:00
assertSoftware ( [ ] fleet . SoftwareTitle {
2025-10-07 21:05:22 +00:00
{ Name : ins0 , Source : "apps" , ExtensionFor : "" } ,
2024-05-14 18:06:33 +00:00
} )
2025-12-08 15:01:07 +00:00
meta , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , * softwareInstallers [ 0 ] . TitleID , false )
require . NoError ( t , err )
require . Equal ( t , displayName , meta . DisplayName )
2024-05-14 18:06:33 +00:00
// add a new installer + ins0 installer
2024-10-23 18:51:02 +00:00
// mark ins0 as install_during_setup
2024-05-14 18:06:33 +00:00
ins1 := "installer1"
ins1File := bytes . NewReader ( [ ] byte ( "installer1" ) )
2024-11-12 14:28:08 +00:00
tfr1 , err := fleet . NewTempFileReader ( ins1File , t . TempDir )
require . NoError ( t , err )
2024-09-20 14:55:47 +00:00
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
2024-05-14 18:06:33 +00:00
{
2024-10-23 18:51:02 +00:00
InstallScript : "install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr0 ,
2024-10-23 18:51:02 +00:00
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-14 18:06:33 +00:00
} ,
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr1 ,
2024-05-14 18:06:33 +00:00
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
2024-08-30 17:13:25 +00:00
UserID : user1 . ID ,
2024-09-06 22:10:28 +00:00
Platform : "darwin" ,
2024-09-17 16:30:27 +00:00
URL : "https://example2.com" ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-14 18:06:33 +00:00
} ,
} )
require . NoError ( t , err )
2024-09-20 14:55:47 +00:00
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
2024-09-06 22:10:28 +00:00
require . Len ( t , softwareInstallers , 2 )
require . NotNil ( t , softwareInstallers [ 0 ] . TitleID )
2024-09-17 16:30:27 +00:00
require . NotNil ( t , softwareInstallers [ 0 ] . TeamID )
require . Equal ( t , team . ID , * softwareInstallers [ 0 ] . TeamID )
require . Equal ( t , "https://example.com" , softwareInstallers [ 0 ] . URL )
2024-09-06 22:10:28 +00:00
require . NotNil ( t , softwareInstallers [ 1 ] . TitleID )
2024-09-17 16:30:27 +00:00
require . NotNil ( t , softwareInstallers [ 1 ] . TeamID )
require . Equal ( t , team . ID , * softwareInstallers [ 1 ] . TeamID )
require . Equal ( t , "https://example2.com" , softwareInstallers [ 1 ] . URL )
2024-05-14 18:06:33 +00:00
assertSoftware ( [ ] fleet . SoftwareTitle {
2025-10-07 21:05:22 +00:00
{ Name : ins0 , Source : "apps" , ExtensionFor : "" } ,
{ Name : ins1 , Source : "apps" , ExtensionFor : "" } ,
2024-05-14 18:06:33 +00:00
} )
2024-10-16 12:26:23 +00:00
// remove ins0 fails due to install_during_setup
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr1 ,
2024-10-16 12:26:23 +00:00
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-10-16 12:26:23 +00:00
} ,
} )
require . Error ( t , err )
require . ErrorIs ( t , err , errDeleteInstallerInstalledDuringSetup )
2024-10-23 18:51:02 +00:00
// batch-set both installers again, this time with nil install_during_setup for ins0,
// will keep it as true.
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr0 ,
2024-10-23 18:51:02 +00:00
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : nil ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-10-23 18:51:02 +00:00
} ,
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr1 ,
2024-10-23 18:51:02 +00:00
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example2.com" ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-10-23 18:51:02 +00:00
} ,
} )
require . NoError ( t , err )
2024-10-16 12:26:23 +00:00
// mark ins0 as NOT install_during_setup
2024-10-23 18:51:02 +00:00
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr0 ,
2024-10-23 18:51:02 +00:00
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
2025-12-08 15:01:07 +00:00
DisplayName : displayName ,
2024-10-23 18:51:02 +00:00
InstallDuringSetup : ptr . Bool ( false ) ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-10-23 18:51:02 +00:00
} ,
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr1 ,
2024-10-23 18:51:02 +00:00
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example2.com" ,
2025-12-08 15:01:07 +00:00
DisplayName : displayName ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-10-23 18:51:02 +00:00
} ,
2024-10-16 12:26:23 +00:00
} )
2024-10-23 18:51:02 +00:00
require . NoError ( t , err )
2025-12-08 15:01:07 +00:00
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 2 )
ins0TitleID := softwareInstallers [ 0 ] . TitleID
2024-10-16 12:26:23 +00:00
2024-05-14 18:06:33 +00:00
// remove ins0
2024-09-20 14:55:47 +00:00
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
2024-05-14 18:06:33 +00:00
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
2024-11-12 14:28:08 +00:00
InstallerFile : tfr1 ,
2024-05-14 18:06:33 +00:00
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
2024-08-30 17:13:25 +00:00
UserID : user1 . ID ,
2025-12-08 15:01:07 +00:00
DisplayName : displayName ,
2024-12-17 19:28:17 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-14 18:06:33 +00:00
} ,
} )
require . NoError ( t , err )
2024-09-20 14:55:47 +00:00
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
2024-09-06 22:10:28 +00:00
require . Len ( t , softwareInstallers , 1 )
require . NotNil ( t , softwareInstallers [ 0 ] . TitleID )
2024-09-17 16:30:27 +00:00
require . NotNil ( t , softwareInstallers [ 0 ] . TeamID )
require . Empty ( t , softwareInstallers [ 0 ] . URL )
2024-05-14 18:06:33 +00:00
assertSoftware ( [ ] fleet . SoftwareTitle {
2025-10-07 21:05:22 +00:00
{ Name : ins1 , Source : "apps" , ExtensionFor : "" } ,
2024-05-14 18:06:33 +00:00
} )
2025-12-08 15:01:07 +00:00
// display name is deleted for ins0
_ , err = ds . getSoftwareTitleDisplayName ( ctx , team . ID , * ins0TitleID )
require . ErrorContains ( t , err , "not found" )
2025-02-11 19:53:11 +00:00
instDetails1 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , * softwareInstallers [ 0 ] . TitleID , false )
require . NoError ( t , err )
// add pending and completed installs for ins1
_ , err = ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , instDetails1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
execID2 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , instDetails1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : host2 . ID ,
InstallUUID : execID2 ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
summary , err := ds . GetSummaryHostSoftwareInstalls ( ctx , instDetails1 . InstallerID )
require . NoError ( t , err )
require . Equal ( t , fleet . SoftwareInstallerStatusSummary { Installed : 1 , PendingInstall : 1 } , * summary )
// batch-set without changes
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
// installs stats haven't changed
summary , err = ds . GetSummaryHostSoftwareInstalls ( ctx , instDetails1 . InstallerID )
require . NoError ( t , err )
require . Equal ( t , fleet . SoftwareInstallerStatusSummary { Installed : 1 , PendingInstall : 1 } , * summary )
// remove ins1 and add ins0
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
// stats don't report anything about ins1 anymore
summary , err = ds . GetSummaryHostSoftwareInstalls ( ctx , instDetails1 . InstallerID )
require . NoError ( t , err )
require . Equal ( t , fleet . SoftwareInstallerStatusSummary { Installed : 0 , PendingInstall : 0 } , * summary )
pendingHost1 , err := ds . ListPendingSoftwareInstalls ( ctx , host1 . ID )
require . NoError ( t , err )
require . Empty ( t , pendingHost1 )
// add pending and completed installs for ins0
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 1 )
instDetails0 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , * softwareInstallers [ 0 ] . TitleID , false )
require . NoError ( t , err )
_ , err = ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , instDetails0 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
execID2b , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , instDetails0 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : host2 . ID ,
InstallUUID : execID2b ,
InstallScriptExitCode : ptr . Int ( 1 ) ,
} )
require . NoError ( t , err )
pendingHost1 , err = ds . ListPendingSoftwareInstalls ( ctx , host1 . ID )
require . NoError ( t , err )
require . Len ( t , pendingHost1 , 1 )
summary , err = ds . GetSummaryHostSoftwareInstalls ( ctx , instDetails0 . InstallerID )
require . NoError ( t , err )
require . Equal ( t , fleet . SoftwareInstallerStatusSummary { FailedInstall : 1 , PendingInstall : 1 } , * summary )
2025-02-11 18:23:20 +00:00
// Add software installer with same name different bundle id
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { {
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer0" ,
Title : "ins0" ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "foo" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
BundleIdentifier : "com.example.different.ins0" ,
} } )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 1 )
assertSoftware ( [ ] fleet . SoftwareTitle {
2025-10-07 21:05:22 +00:00
{ Name : ins0 , Source : "apps" , ExtensionFor : "" , BundleIdentifier : ptr . String ( "com.example.different.ins0" ) } ,
2025-02-11 18:23:20 +00:00
} )
// Add software installer with the same bundle id but different name
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { {
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer0" ,
Title : "ins0-different" ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "foo" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
BundleIdentifier : "com.example.ins0" ,
} } )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 1 )
assertSoftware ( [ ] fleet . SoftwareTitle {
2025-10-07 21:05:22 +00:00
{ Name : "ins0-different" , Source : "apps" , ExtensionFor : "" , BundleIdentifier : ptr . String ( "com.example.ins0" ) } ,
2025-02-11 18:23:20 +00:00
} )
2024-05-14 18:06:33 +00:00
// remove everything
2024-09-20 14:55:47 +00:00
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { } )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
2024-05-14 18:06:33 +00:00
require . NoError ( t , err )
2024-09-06 22:10:28 +00:00
require . Empty ( t , softwareInstallers )
2024-05-14 18:06:33 +00:00
assertSoftware ( [ ] fleet . SoftwareTitle { } )
2025-02-11 19:53:11 +00:00
// stats don't report anything about ins0 anymore
summary , err = ds . GetSummaryHostSoftwareInstalls ( ctx , instDetails0 . InstallerID )
require . NoError ( t , err )
require . Equal ( t , fleet . SoftwareInstallerStatusSummary { FailedInstall : 0 , PendingInstall : 0 } , * summary )
pendingHost1 , err = ds . ListPendingSoftwareInstalls ( ctx , host1 . ID )
require . NoError ( t , err )
require . Empty ( t , pendingHost1 )
2024-05-14 18:06:33 +00:00
}
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355)
Still adding tests but wanted to get this up for review of the overall
"shape" of the fix
When certain things happen like installer updates we delete pending
upcoming_activities(UA) and host_software_install(HSI) entries and need
to mark setup_experience_status_results(SESR) cancelled. When this
happens if that UA/HSI are being depended on by setup experience we need
to make sure that that setup experience result eventually gets marked
failed.
I kind of went back and forth a few times on how best to do this and
avoid race conditions. One thing I tried was looking at existence of the
UA/HSI but found that naively just trying to look at that in relation to
the SESR entry seemed to have a few race conditions that were hard to
resolve. There are a few possible states here we need to account for
such as:
un-activated, totally not yet running software install cancelled
activated but not yet running on the host software install cancelled
activated and running on the host software install cancelled before
results are completely reported back
What I eventually came around to was essentially that we want to mark
the SESR cancelled in the same transaction we delete the HSI/UA in. We
then finalize it by marking it failed and sending the activity the next
time the host fetches setupm experience results. The new cancelled
status never leaves fleet. This is a bit ugly but in my testing avoided
the race conditions and works well.
Note that to actually avoid setup experience hanging entirely we still
need to fix #29357 which encompasses several cases where the unified
queue can get completely stuck for a host
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [ ] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [ ] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
- [ ] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
func testBatchSetSoftwareInstallersSetupExperienceSideEffects ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
t . Cleanup ( func ( ) { ds . testActivateSpecificNextActivities = nil } )
// create a team
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : t . Name ( ) } )
require . NoError ( t , err )
// create a host
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
2025-07-17 14:20:49 +00:00
err = ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( & team . ID , [ ] uint { host1 . ID } ) )
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355)
Still adding tests but wanted to get this up for review of the overall
"shape" of the fix
When certain things happen like installer updates we delete pending
upcoming_activities(UA) and host_software_install(HSI) entries and need
to mark setup_experience_status_results(SESR) cancelled. When this
happens if that UA/HSI are being depended on by setup experience we need
to make sure that that setup experience result eventually gets marked
failed.
I kind of went back and forth a few times on how best to do this and
avoid race conditions. One thing I tried was looking at existence of the
UA/HSI but found that naively just trying to look at that in relation to
the SESR entry seemed to have a few race conditions that were hard to
resolve. There are a few possible states here we need to account for
such as:
un-activated, totally not yet running software install cancelled
activated but not yet running on the host software install cancelled
activated and running on the host software install cancelled before
results are completely reported back
What I eventually came around to was essentially that we want to mark
the SESR cancelled in the same transaction we delete the HSI/UA in. We
then finalize it by marking it failed and sending the activity the next
time the host fetches setupm experience results. The new cancelled
status never leaves fleet. This is a bit ugly but in my testing avoided
the race conditions and works well.
Note that to actually avoid setup experience hanging entirely we still
need to fix #29357 which encompasses several cases where the unified
queue can get completely stuck for a host
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [ ] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [ ] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
- [ ] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
host1 . TeamID = & team . ID
require . NoError ( t , err )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
assertSoftware := func ( wantTitles [ ] fleet . SoftwareTitle ) {
tmFilter := fleet . TeamFilter { User : & fleet . User { GlobalRole : ptr . String ( fleet . RoleAdmin ) } }
titles , _ , _ , err := ds . ListSoftwareTitles (
ctx ,
fleet . SoftwareTitleListOptions { TeamID : & team . ID } ,
tmFilter ,
)
require . NoError ( t , err )
require . Len ( t , titles , len ( wantTitles ) )
for _ , title := range titles {
meta , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , title . ID , false )
require . NoError ( t , err )
require . NotNil ( t , meta . TitleID )
}
}
// add two installers
ins0 := "installer0"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
ins1 := "installer1"
ins1File := bytes . NewReader ( [ ] byte ( "installer1" ) )
tfr1 , err := fleet . NewTempFileReader ( ins1File , t . TempDir )
require . NoError ( t , err )
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example2.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
softwareInstallers , err := ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 2 )
require . NotNil ( t , softwareInstallers [ 0 ] . TitleID )
require . NotNil ( t , softwareInstallers [ 0 ] . TeamID )
require . Equal ( t , team . ID , * softwareInstallers [ 0 ] . TeamID )
require . Equal ( t , "https://example.com" , softwareInstallers [ 0 ] . URL )
require . NotNil ( t , softwareInstallers [ 1 ] . TitleID )
require . NotNil ( t , softwareInstallers [ 1 ] . TeamID )
require . Equal ( t , team . ID , * softwareInstallers [ 1 ] . TeamID )
require . Equal ( t , "https://example2.com" , softwareInstallers [ 1 ] . URL )
assertSoftware ( [ ] fleet . SoftwareTitle {
2025-10-07 21:05:22 +00:00
{ Name : ins0 , Source : "apps" , ExtensionFor : "" } ,
{ Name : ins1 , Source : "apps" , ExtensionFor : "" } ,
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355)
Still adding tests but wanted to get this up for review of the overall
"shape" of the fix
When certain things happen like installer updates we delete pending
upcoming_activities(UA) and host_software_install(HSI) entries and need
to mark setup_experience_status_results(SESR) cancelled. When this
happens if that UA/HSI are being depended on by setup experience we need
to make sure that that setup experience result eventually gets marked
failed.
I kind of went back and forth a few times on how best to do this and
avoid race conditions. One thing I tried was looking at existence of the
UA/HSI but found that naively just trying to look at that in relation to
the SESR entry seemed to have a few race conditions that were hard to
resolve. There are a few possible states here we need to account for
such as:
un-activated, totally not yet running software install cancelled
activated but not yet running on the host software install cancelled
activated and running on the host software install cancelled before
results are completely reported back
What I eventually came around to was essentially that we want to mark
the SESR cancelled in the same transaction we delete the HSI/UA in. We
then finalize it by marking it failed and sending the activity the next
time the host fetches setupm experience results. The new cancelled
status never leaves fleet. This is a bit ugly but in my testing avoided
the race conditions and works well.
Note that to actually avoid setup experience hanging entirely we still
need to fix #29357 which encompasses several cases where the unified
queue can get completely stuck for a host
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [ ] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [ ] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
- [ ] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
} )
// Add setup_experience_status_results for both installers
2025-09-04 15:58:47 +00:00
_ , err = ds . EnqueueSetupExperienceItems ( ctx , "darwin" , host1 . UUID , * host1 . TeamID )
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355)
Still adding tests but wanted to get this up for review of the overall
"shape" of the fix
When certain things happen like installer updates we delete pending
upcoming_activities(UA) and host_software_install(HSI) entries and need
to mark setup_experience_status_results(SESR) cancelled. When this
happens if that UA/HSI are being depended on by setup experience we need
to make sure that that setup experience result eventually gets marked
failed.
I kind of went back and forth a few times on how best to do this and
avoid race conditions. One thing I tried was looking at existence of the
UA/HSI but found that naively just trying to look at that in relation to
the SESR entry seemed to have a few race conditions that were hard to
resolve. There are a few possible states here we need to account for
such as:
un-activated, totally not yet running software install cancelled
activated but not yet running on the host software install cancelled
activated and running on the host software install cancelled before
results are completely reported back
What I eventually came around to was essentially that we want to mark
the SESR cancelled in the same transaction we delete the HSI/UA in. We
then finalize it by marking it failed and sending the activity the next
time the host fetches setupm experience results. The new cancelled
status never leaves fleet. This is a bit ugly but in my testing avoided
the race conditions and works well.
Note that to actually avoid setup experience hanging entirely we still
need to fix #29357 which encompasses several cases where the unified
queue can get completely stuck for a host
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
<!-- Note that API documentation changes are now addressed by the
product design team. -->
- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [ ] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [ ] Added/updated automated tests
- [ ] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [ ] Make sure fleetd is compatible with the latest released version of
Fleet (see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)).
- [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
- [ ] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
require . NoError ( t , err )
statuses , err := ds . ListSetupExperienceResultsByHostUUID ( ctx , host1 . UUID )
require . NoError ( t , err )
require . Len ( t , statuses , 2 )
// Enqueue the actual install requests
for _ , status := range statuses {
execID , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , * status . SoftwareInstallerID , fleet . HostSoftwareInstallOptions { ForSetupExperience : true } )
require . NoError ( t , err )
status . HostSoftwareInstallsExecutionID = & execID
status . Status = fleet . SetupExperienceStatusRunning
err = ds . UpdateSetupExperienceStatusResult ( ctx , status )
require . NoError ( t , err )
}
// batch-set without changes
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example2.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
statuses , err = ds . ListSetupExperienceResultsByHostUUID ( ctx , host1 . UUID )
require . NoError ( t , err )
require . Len ( t , statuses , 2 )
for _ , status := range statuses {
require . Equal ( t , fleet . SetupExperienceStatusRunning , status . Status )
}
// batch-set change ins0's install script to update it and cancel the pending install
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install2" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
PostInstallScript : "post-install" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example2.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
statuses , err = ds . ListSetupExperienceResultsByHostUUID ( ctx , host1 . UUID )
require . NoError ( t , err )
require . Len ( t , statuses , 2 )
// Verify that ins0's install was cancelled but ins1 is still running
ins1ExecID := ""
ins0Found := false
ins1Found := false
for _ , status := range statuses {
if status . Name == ins0 {
assert . False ( t , ins0Found , "duplicate ins0 found" )
ins0Found = true
require . Equal ( t , fleet . SetupExperienceStatusCancelled , status . Status )
} else {
assert . False ( t , ins1Found , "duplicate ins1 found" )
assert . Equal ( t , ins1 , status . Name )
require . Equal ( t , fleet . SetupExperienceStatusRunning , status . Status )
require . NotNil ( t , status . HostSoftwareInstallsExecutionID )
ins1ExecID = * status . HostSoftwareInstallsExecutionID
}
}
// activate and set a result for ins1 as if the install completed
ds . testActivateSpecificNextActivities = [ ] string { ins1ExecID }
_ , err = ds . activateNextUpcomingActivity ( ctx , ds . writer ( ctx ) , host1 . ID , "" )
require . NoError ( t , err )
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
HostID : host1 . ID ,
InstallUUID : ins1ExecID ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
// batch-set change ins1's install script to update it. This should do nothing to the setup
// experience result because the install already completed
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install2" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : ins0 ,
Title : ins0 ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install3" ,
PostInstallScript : "post-install" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : ins1 ,
Title : ins1 ,
Source : "apps" ,
Version : "2" ,
PreInstallQuery : "select 1 from bar;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example2.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
statuses , err = ds . ListSetupExperienceResultsByHostUUID ( ctx , host1 . UUID )
require . NoError ( t , err )
require . Len ( t , statuses , 2 )
// Verify that ins0's install is still cancelled and ins1 is still running(because it hasn't
// been updated in the SESR entry yet)
ins0Found = false
ins1Found = false
for _ , status := range statuses {
if status . Name == ins0 {
assert . False ( t , ins0Found , "duplicate ins0 found" )
ins0Found = true
require . Equal ( t , fleet . SetupExperienceStatusCancelled , status . Status )
} else {
assert . False ( t , ins1Found , "duplicate ins1 found" )
assert . Equal ( t , ins1 , status . Name )
require . Equal ( t , fleet . SetupExperienceStatusRunning , status . Status )
}
}
}
2024-05-10 18:44:49 +00:00
func testGetSoftwareInstallerMetadataByTeamAndTitleID ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 2" } )
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2024-05-10 18:44:49 +00:00
2024-12-11 14:54:15 +00:00
installerID , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-05-10 18:44:49 +00:00
Title : "foo" ,
Source : "bar" ,
InstallScript : "echo install" ,
PostInstallScript : "echo post-install" ,
PreInstallQuery : "SELECT 1" ,
TeamID : & team . ID ,
Filename : "foo.pkg" ,
2024-08-30 17:13:25 +00:00
Platform : "darwin" ,
UserID : user1 . ID ,
2024-12-17 00:17:13 +00:00
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-10 18:44:49 +00:00
} )
require . NoError ( t , err )
2024-05-15 12:40:06 +00:00
installerMeta , err := ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
2024-05-10 18:44:49 +00:00
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
require . Equal ( t , "darwin" , installerMeta . Platform )
2024-05-10 18:44:49 +00:00
2024-05-15 12:40:06 +00:00
metaByTeamAndTitle , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , * installerMeta . TitleID , true )
2024-05-10 18:44:49 +00:00
require . NoError ( t , err )
require . Equal ( t , "echo install" , metaByTeamAndTitle . InstallScript )
require . Equal ( t , "echo post-install" , metaByTeamAndTitle . PostInstallScript )
require . EqualValues ( t , installerID , metaByTeamAndTitle . InstallerID )
require . Equal ( t , "SELECT 1" , metaByTeamAndTitle . PreInstallQuery )
2024-12-11 14:54:15 +00:00
installerID , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
Title : "bar" ,
Source : "bar" ,
InstallScript : "echo install" ,
TeamID : & team . ID ,
Filename : "foo.pkg" ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-05-10 18:44:49 +00:00
} )
require . NoError ( t , err )
2024-05-15 12:40:06 +00:00
installerMeta , err = ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
2024-05-10 18:44:49 +00:00
require . NoError ( t , err )
2024-05-15 12:40:06 +00:00
metaByTeamAndTitle , err = ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , * installerMeta . TitleID , true )
2024-05-10 18:44:49 +00:00
require . NoError ( t , err )
require . Equal ( t , "echo install" , metaByTeamAndTitle . InstallScript )
require . Equal ( t , "" , metaByTeamAndTitle . PostInstallScript )
require . EqualValues ( t , installerID , metaByTeamAndTitle . InstallerID )
require . Equal ( t , "" , metaByTeamAndTitle . PreInstallQuery )
}
2024-07-02 16:32:49 +00:00
func testHasSelfServiceSoftwareInstallers ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 2" } )
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
2024-07-02 16:32:49 +00:00
2024-09-06 13:14:09 +00:00
test . CreateInsertGlobalVPPToken ( t , ds )
2024-07-02 16:32:49 +00:00
const platform = "linux"
// No installers
hasSelfService , err := ds . HasSelfServiceSoftwareInstallers ( ctx , platform , nil )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , & team . ID )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
// Create a non-self service installer
2024-12-11 14:54:15 +00:00
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
Title : "foo" ,
Source : "bar" ,
InstallScript : "echo install" ,
TeamID : & team . ID ,
Filename : "foo.pkg" ,
Platform : platform ,
SelfService : false ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-07-02 16:32:49 +00:00
} )
require . NoError ( t , err )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , nil )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , & team . ID )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
// Create a self-service installer for team
2024-12-11 14:54:15 +00:00
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
Title : "foo2" ,
Source : "bar2" ,
InstallScript : "echo install" ,
TeamID : & team . ID ,
Filename : "foo2.pkg" ,
Platform : platform ,
SelfService : true ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-07-02 16:32:49 +00:00
} )
require . NoError ( t , err )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , nil )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , & team . ID )
require . NoError ( t , err )
assert . True ( t , hasSelfService )
2024-08-21 20:40:01 +00:00
// Create a non self-service VPP for global/linux (not truly possible as VPP is Apple but for testing)
_ , err = ds . InsertVPPAppWithTeam ( ctx , & fleet . VPPApp { VPPAppTeam : fleet . VPPAppTeam { VPPAppID : fleet . VPPAppID { AdamID : "adam_vpp_1" , Platform : platform } } , Name : "vpp1" , BundleIdentifier : "com.app.vpp1" } , nil )
require . NoError ( t , err )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , nil )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , & team . ID )
require . NoError ( t , err )
assert . True ( t , hasSelfService )
// Create a self-service VPP for global/linux (not truly possible as VPP is Apple but for testing)
_ , err = ds . InsertVPPAppWithTeam ( ctx , & fleet . VPPApp { VPPAppTeam : fleet . VPPAppTeam { VPPAppID : fleet . VPPAppID { AdamID : "adam_vpp_2" , Platform : platform } , SelfService : true } , Name : "vpp2" , BundleIdentifier : "com.app.vpp2" } , nil )
require . NoError ( t , err )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , nil )
require . NoError ( t , err )
assert . True ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , platform , & team . ID )
require . NoError ( t , err )
assert . True ( t , hasSelfService )
2024-07-02 16:32:49 +00:00
// Create a global self-service installer
2024-12-11 14:54:15 +00:00
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
Title : "foo global" ,
Source : "bar" ,
InstallScript : "echo install" ,
TeamID : nil ,
Filename : "foo global.pkg" ,
Platform : platform ,
SelfService : true ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-07-02 16:32:49 +00:00
} )
require . NoError ( t , err )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "ubuntu" , nil )
require . NoError ( t , err )
assert . True ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "ubuntu" , & team . ID )
require . NoError ( t , err )
assert . True ( t , hasSelfService )
2024-08-21 20:40:01 +00:00
// Create a self-service VPP for team/darwin
_ , err = ds . InsertVPPAppWithTeam ( ctx , & fleet . VPPApp { VPPAppTeam : fleet . VPPAppTeam { VPPAppID : fleet . VPPAppID { AdamID : "adam_vpp_3" , Platform : fleet . MacOSPlatform } , SelfService : true } , Name : "vpp3" , BundleIdentifier : "com.app.vpp3" } , & team . ID )
require . NoError ( t , err )
// Check darwin
2024-07-02 16:32:49 +00:00
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "darwin" , nil )
require . NoError ( t , err )
assert . False ( t , hasSelfService )
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "darwin" , & team . ID )
require . NoError ( t , err )
2024-08-21 20:40:01 +00:00
assert . True ( t , hasSelfService )
2024-07-02 16:32:49 +00:00
}
2024-08-30 17:13:25 +00:00
2024-10-16 12:26:23 +00:00
func testDeleteSoftwareInstallers ( t * testing . T , ds * Datastore ) {
2024-08-30 17:13:25 +00:00
ctx := context . Background ( )
dir := t . TempDir ( )
store , err := filesystem . NewSoftwareInstallerStore ( dir )
require . NoError ( t , err )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
// put an installer and save it in the DB
ins0 := "installer.pkg"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
err = store . Put ( ctx , ins0 , ins0File )
require . NoError ( t , err )
2024-11-12 14:28:08 +00:00
_ , _ = ins0File . Seek ( 0 , 0 )
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
team1 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team1" } )
require . NoError ( t , err )
2024-12-11 14:54:15 +00:00
softwareInstallerID , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer.pkg" ,
Title : "ins0" ,
Source : "apps" ,
Platform : "darwin" ,
TeamID : & team1 . ID ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-08-30 17:13:25 +00:00
} )
require . NoError ( t , err )
p1 , err := ds . NewTeamPolicy ( ctx , team1 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "p1" ,
Query : "SELECT 1;" ,
SoftwareInstallerID : & softwareInstallerID ,
} )
require . NoError ( t , err )
err = ds . DeleteSoftwareInstaller ( ctx , softwareInstallerID )
require . Error ( t , err )
require . ErrorIs ( t , err , errDeleteInstallerWithAssociatedPolicy )
_ , err = ds . DeleteTeamPolicies ( ctx , team1 . ID , [ ] uint { p1 . ID } )
require . NoError ( t , err )
2024-10-16 12:26:23 +00:00
// mark the installer as "installed during setup", which prevents deletion
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` UPDATE software_installers SET install_during_setup = 1 WHERE id = ? ` , softwareInstallerID )
return err
} )
err = ds . DeleteSoftwareInstaller ( ctx , softwareInstallerID )
require . Error ( t , err )
require . ErrorIs ( t , err , errDeleteInstallerInstalledDuringSetup )
// clear "installed during setup", which allows deletion
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` UPDATE software_installers SET install_during_setup = 0 WHERE id = ? ` , softwareInstallerID )
return err
} )
2024-08-30 17:13:25 +00:00
err = ds . DeleteSoftwareInstaller ( ctx , softwareInstallerID )
require . NoError ( t , err )
2024-10-16 12:26:23 +00:00
// deleting again returns an error, no such installer
err = ds . DeleteSoftwareInstaller ( ctx , softwareInstallerID )
2025-02-18 21:28:54 +00:00
var nfe * common_mysql . NotFoundError
2024-10-16 12:26:23 +00:00
require . ErrorAs ( t , err , & nfe )
2024-08-30 17:13:25 +00:00
}
2024-12-16 17:47:34 +00:00
func testDeletePendingSoftwareInstallsForPolicy ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
host2 := test . NewHost ( t , ds , "host2" , "2" , "host2key" , "host2uuid" , time . Now ( ) )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
team1 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team1" } )
require . NoError ( t , err )
dir := t . TempDir ( )
store , err := filesystem . NewSoftwareInstallerStore ( dir )
require . NoError ( t , err )
ins0 := "installer.pkg"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
err = store . Put ( ctx , ins0 , ins0File )
require . NoError ( t , err )
_ , _ = ins0File . Seek ( 0 , 0 )
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
installerID1 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer.pkg" ,
Title : "ins0" ,
Source : "apps" ,
Platform : "darwin" ,
TeamID : & team1 . ID ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-12-16 17:47:34 +00:00
} )
require . NoError ( t , err )
policy1 , err := ds . NewTeamPolicy ( ctx , team1 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "p1" ,
Query : "SELECT 1;" ,
SoftwareInstallerID : & installerID1 ,
} )
require . NoError ( t , err )
installerID2 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer.pkg" ,
Title : "ins1" ,
Source : "apps" ,
Platform : "darwin" ,
TeamID : & team1 . ID ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-12-16 17:47:34 +00:00
} )
require . NoError ( t , err )
policy2 , err := ds . NewTeamPolicy ( ctx , team1 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "p2" ,
Query : "SELECT 2;" ,
SoftwareInstallerID : & installerID2 ,
} )
require . NoError ( t , err )
const hostSoftwareInstallsCount = "SELECT count(1) FROM host_software_installs WHERE status = ? and execution_id = ?"
var count int
// install for correct policy & correct status
2025-02-11 19:53:11 +00:00
executionID , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , installerID1 , fleet . HostSoftwareInstallOptions { PolicyID : & policy1 . ID } )
2024-12-16 17:47:34 +00:00
require . NoError ( t , err )
err = sqlx . GetContext ( ctx , ds . reader ( ctx ) , & count , hostSoftwareInstallsCount , fleet . SoftwareInstallPending , executionID )
require . NoError ( t , err )
require . Equal ( t , 1 , count )
err = ds . deletePendingSoftwareInstallsForPolicy ( ctx , & team1 . ID , policy1 . ID )
require . NoError ( t , err )
err = sqlx . GetContext ( ctx , ds . reader ( ctx ) , & count , hostSoftwareInstallsCount , fleet . SoftwareInstallPending , executionID )
require . NoError ( t , err )
require . Equal ( t , 0 , count )
// install for different policy & correct status
2025-02-11 19:53:11 +00:00
executionID , err = ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , installerID2 , fleet . HostSoftwareInstallOptions { PolicyID : & policy2 . ID } )
2024-12-16 17:47:34 +00:00
require . NoError ( t , err )
err = sqlx . GetContext ( ctx , ds . reader ( ctx ) , & count , hostSoftwareInstallsCount , fleet . SoftwareInstallPending , executionID )
require . NoError ( t , err )
require . Equal ( t , 1 , count )
err = ds . deletePendingSoftwareInstallsForPolicy ( ctx , & team1 . ID , policy1 . ID )
require . NoError ( t , err )
err = sqlx . GetContext ( ctx , ds . reader ( ctx ) , & count , hostSoftwareInstallsCount , fleet . SoftwareInstallPending , executionID )
require . NoError ( t , err )
require . Equal ( t , 1 , count )
// install for correct policy & incorrect status
2025-02-11 19:53:11 +00:00
executionID , err = ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , installerID1 , fleet . HostSoftwareInstallOptions { PolicyID : & policy1 . ID } )
2024-12-16 17:47:34 +00:00
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-12-16 17:47:34 +00:00
HostID : host2 . ID ,
InstallUUID : executionID ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
err = ds . deletePendingSoftwareInstallsForPolicy ( ctx , & team1 . ID , policy1 . ID )
require . NoError ( t , err )
err = sqlx . GetContext ( ctx , ds . reader ( ctx ) , & count , ` SELECT count(1) FROM host_software_installs WHERE execution_id = ? ` , executionID )
require . NoError ( t , err )
require . Equal ( t , 1 , count )
}
2024-08-30 17:13:25 +00:00
func testGetHostLastInstallData ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
team1 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team1" } )
require . NoError ( t , err )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) , test . WithTeamID ( team1 . ID ) )
host2 := test . NewHost ( t , ds , "host2" , "2" , "host2key" , "host2uuid" , time . Now ( ) , test . WithTeamID ( team1 . ID ) )
dir := t . TempDir ( )
store , err := filesystem . NewSoftwareInstallerStore ( dir )
require . NoError ( t , err )
// put an installer and save it in the DB
ins0 := "installer.pkg"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
err = store . Put ( ctx , ins0 , ins0File )
require . NoError ( t , err )
2024-11-12 14:28:08 +00:00
_ , _ = ins0File . Seek ( 0 , 0 )
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
2024-08-30 17:13:25 +00:00
2024-12-11 14:54:15 +00:00
softwareInstallerID1 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
InstallScript : "install" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer.pkg" ,
Title : "ins1" ,
Source : "apps" ,
Platform : "darwin" ,
TeamID : & team1 . ID ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-08-30 17:13:25 +00:00
} )
require . NoError ( t , err )
2024-12-11 14:54:15 +00:00
softwareInstallerID2 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2024-12-17 00:17:13 +00:00
InstallScript : "install2" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer2.pkg" ,
Title : "ins2" ,
Source : "apps" ,
Platform : "darwin" ,
TeamID : & team1 . ID ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2024-08-30 17:13:25 +00:00
} )
require . NoError ( t , err )
// No installations on host1 yet.
host1LastInstall , err := ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . Nil ( t , host1LastInstall )
// Install installer.pkg on host1.
2025-02-11 19:53:11 +00:00
installUUID1 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , softwareInstallerID1 , fleet . HostSoftwareInstallOptions { } )
2024-08-30 17:13:25 +00:00
require . NoError ( t , err )
require . NotEmpty ( t , installUUID1 )
// Last installation should be pending.
host1LastInstall , err = ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . NotNil ( t , host1LastInstall )
require . Equal ( t , installUUID1 , host1LastInstall . ExecutionID )
require . NotNil ( t , host1LastInstall . Status )
2024-09-04 21:46:48 +00:00
require . Equal ( t , fleet . SoftwareInstallPending , * host1LastInstall . Status )
2024-08-30 17:13:25 +00:00
// Set result of last installation.
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-08-30 17:13:25 +00:00
HostID : host1 . ID ,
InstallUUID : installUUID1 ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
// Last installation should be "installed".
host1LastInstall , err = ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . NotNil ( t , host1LastInstall )
require . Equal ( t , installUUID1 , host1LastInstall . ExecutionID )
require . NotNil ( t , host1LastInstall . Status )
2024-09-04 21:46:48 +00:00
require . Equal ( t , fleet . SoftwareInstalled , * host1LastInstall . Status )
2024-08-30 17:13:25 +00:00
// Install installer2.pkg on host1.
2025-02-11 19:53:11 +00:00
installUUID2 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , softwareInstallerID2 , fleet . HostSoftwareInstallOptions { } )
2024-08-30 17:13:25 +00:00
require . NoError ( t , err )
require . NotEmpty ( t , installUUID2 )
// Last installation for installer1.pkg should be "installed".
host1LastInstall , err = ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . NotNil ( t , host1LastInstall )
require . Equal ( t , installUUID1 , host1LastInstall . ExecutionID )
require . NotNil ( t , host1LastInstall . Status )
2024-09-04 21:46:48 +00:00
require . Equal ( t , fleet . SoftwareInstalled , * host1LastInstall . Status )
2024-08-30 17:13:25 +00:00
// Last installation for installer2.pkg should be "pending".
host1LastInstall , err = ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID2 )
require . NoError ( t , err )
require . NotNil ( t , host1LastInstall )
require . Equal ( t , installUUID2 , host1LastInstall . ExecutionID )
require . NotNil ( t , host1LastInstall . Status )
2024-09-04 21:46:48 +00:00
require . Equal ( t , fleet . SoftwareInstallPending , * host1LastInstall . Status )
2024-08-30 17:13:25 +00:00
// Perform another installation of installer1.pkg.
2025-02-11 19:53:11 +00:00
installUUID3 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , softwareInstallerID1 , fleet . HostSoftwareInstallOptions { } )
2024-08-30 17:13:25 +00:00
require . NoError ( t , err )
require . NotEmpty ( t , installUUID3 )
// Last installation for installer1.pkg should be "pending" again.
host1LastInstall , err = ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . NotNil ( t , host1LastInstall )
require . Equal ( t , installUUID3 , host1LastInstall . ExecutionID )
require . NotNil ( t , host1LastInstall . Status )
2024-09-04 21:46:48 +00:00
require . Equal ( t , fleet . SoftwareInstallPending , * host1LastInstall . Status )
2024-08-30 17:13:25 +00:00
2025-02-11 19:53:11 +00:00
// Set result of last installer1.pkg installation, but first we need to set a
// result for installUUID2 so that this last installer1.pkg request is
// activated.
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : host1 . ID ,
InstallUUID : installUUID2 ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2024-08-30 17:13:25 +00:00
HostID : host1 . ID ,
InstallUUID : installUUID3 ,
InstallScriptExitCode : ptr . Int ( 1 ) ,
} )
require . NoError ( t , err )
// Last installation for installer1.pkg should be "failed".
host1LastInstall , err = ds . GetHostLastInstallData ( ctx , host1 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . NotNil ( t , host1LastInstall )
require . Equal ( t , installUUID3 , host1LastInstall . ExecutionID )
require . NotNil ( t , host1LastInstall . Status )
2024-09-04 21:46:48 +00:00
require . Equal ( t , fleet . SoftwareInstallFailed , * host1LastInstall . Status )
2024-08-30 17:13:25 +00:00
// No installations on host2.
host2LastInstall , err := ds . GetHostLastInstallData ( ctx , host2 . ID , softwareInstallerID1 )
require . NoError ( t , err )
require . Nil ( t , host2LastInstall )
host2LastInstall , err = ds . GetHostLastInstallData ( ctx , host2 . ID , softwareInstallerID2 )
require . NoError ( t , err )
require . Nil ( t , host2LastInstall )
}
2024-09-26 18:23:50 +00:00
func testGetOrGenerateSoftwareInstallerTitleID ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
host1 := test . NewHost ( t , ds , "host1" , "" , "host1key" , "host1uuid" , time . Now ( ) )
host2 := test . NewHost ( t , ds , "host2" , "" , "host2key" , "host2uuid" , time . Now ( ) )
software1 := [ ] fleet . Software {
{ Name : "Existing Title" , Version : "0.0.1" , Source : "apps" , BundleIdentifier : "existing.title" } ,
}
software2 := [ ] fleet . Software {
{ Name : "Existing Title" , Version : "v0.0.2" , Source : "apps" , BundleIdentifier : "existing.title" } ,
{ Name : "Existing Title" , Version : "0.0.3" , Source : "apps" , BundleIdentifier : "existing.title" } ,
{ Name : "Existing Title Without Bundle" , Version : "0.0.3" , Source : "apps" } ,
}
_ , err := ds . UpdateHostSoftware ( ctx , host1 . ID , software1 )
require . NoError ( t , err )
_ , err = ds . UpdateHostSoftware ( ctx , host2 . ID , software2 )
require . NoError ( t , err )
require . NoError ( t , ds . SyncHostsSoftware ( ctx , time . Now ( ) ) )
require . NoError ( t , ds . SyncHostsSoftwareTitles ( ctx , time . Now ( ) ) )
tests := [ ] struct {
name string
payload * fleet . UploadSoftwareInstallerPayload
} {
{
name : "title that already exists, no bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title" ,
Source : "apps" ,
} ,
} ,
{
name : "title that already exists, mismatched bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title" ,
Source : "apps" ,
BundleIdentifier : "com.existing.bundle" ,
} ,
} ,
{
name : "title that already exists but doesn't have a bundle identifier" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title Without Bundle" ,
Source : "apps" ,
} ,
} ,
{
name : "title that already exists, no bundle identifier in DB, bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title Without Bundle" ,
Source : "apps" ,
BundleIdentifier : "com.new.bundleid" ,
} ,
} ,
{
name : "title that doesn't exist, no bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "New Title" ,
Source : "some_source" ,
} ,
} ,
{
name : "title that doesn't exist, with bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "New Title With Bundle" ,
Source : "some_source" ,
BundleIdentifier : "com.new.bundle" ,
} ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
id , err := ds . getOrGenerateSoftwareInstallerTitleID ( ctx , tt . payload )
require . NoError ( t , err )
require . NotEmpty ( t , id )
} )
}
}
2024-12-17 19:28:17 +00:00
func testBatchSetSoftwareInstallersScopedViaLabels ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
// create a host to have a pending install request
host := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
// create a couple teams and a user
tm1 , err := ds . NewTeam ( ctx , & fleet . Team { Name : t . Name ( ) + "1" } )
require . NoError ( t , err )
tm2 , err := ds . NewTeam ( ctx , & fleet . Team { Name : t . Name ( ) + "2" } )
require . NoError ( t , err )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
// create some installer payloads to be used by test cases
installers := make ( [ ] * fleet . UploadSoftwareInstallerPayload , 3 )
for i := range installers {
file := bytes . NewReader ( [ ] byte ( "installer" + fmt . Sprint ( i ) ) )
tfr , err := fleet . NewTempFileReader ( file , t . TempDir )
require . NoError ( t , err )
installers [ i ] = & fleet . UploadSoftwareInstallerPayload {
InstallScript : "install" ,
InstallerFile : tfr ,
StorageID : "installer" + fmt . Sprint ( i ) ,
Filename : "installer" + fmt . Sprint ( i ) ,
Title : "ins" + fmt . Sprint ( i ) ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "foo" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
}
}
// create some labels to be used by test cases
labels := make ( [ ] * fleet . Label , 4 )
for i := range labels {
lbl , err := ds . NewLabel ( ctx , & fleet . Label { Name : "label" + fmt . Sprint ( i ) } )
require . NoError ( t , err )
labels [ i ] = lbl
}
type testPayload struct {
Installer * fleet . UploadSoftwareInstallerPayload
Labels [ ] * fleet . Label
Exclude bool
ShouldCancelPending * bool // nil if the installer is new (could not have pending), otherwise true/false if it was edited
}
// test scenarios - note that subtests must NOT be used as the sequence of
// tests matters - they cannot be run in isolation.
cases := [ ] struct {
desc string
team * fleet . Team
payload [ ] testPayload
} {
{
desc : "empty payload" ,
payload : nil ,
} ,
{
desc : "no team, installer0, no label" ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] } ,
} ,
} ,
{
desc : "team 1, installer0, include label0" ,
team : tm1 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 0 ] } } ,
} ,
} ,
{
desc : "no team, installer0 no change, add installer1 with exclude label1" ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , ShouldCancelPending : ptr . Bool ( false ) } ,
{ Installer : installers [ 1 ] , Labels : [ ] * fleet . Label { labels [ 1 ] } , Exclude : true } ,
} ,
} ,
{
desc : "no team, installer0 no change, installer1 change to include label1" ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , ShouldCancelPending : ptr . Bool ( false ) } ,
{ Installer : installers [ 1 ] , Labels : [ ] * fleet . Label { labels [ 1 ] } , Exclude : false , ShouldCancelPending : ptr . Bool ( true ) } ,
} ,
} ,
{
desc : "team 1, installer0, include label0 and add label1" ,
team : tm1 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 0 ] , labels [ 1 ] } , ShouldCancelPending : ptr . Bool ( true ) } ,
} ,
} ,
{
desc : "team 1, installer0, remove label0 and keep label1" ,
team : tm1 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 1 ] } , ShouldCancelPending : ptr . Bool ( true ) } ,
} ,
} ,
{
desc : "team 1, installer0, switch to label0 and label2" ,
team : tm1 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 0 ] , labels [ 2 ] } , ShouldCancelPending : ptr . Bool ( true ) } ,
} ,
} ,
{
desc : "team 2, 3 installers, mix of labels" ,
team : tm2 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 0 ] } , Exclude : false } ,
{ Installer : installers [ 1 ] , Labels : [ ] * fleet . Label { labels [ 0 ] , labels [ 1 ] , labels [ 2 ] } , Exclude : true } ,
{ Installer : installers [ 2 ] , Labels : [ ] * fleet . Label { labels [ 1 ] , labels [ 2 ] } , Exclude : false } ,
} ,
} ,
{
desc : "team 1, installer0 no change and add installer2" ,
team : tm1 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 0 ] , labels [ 2 ] } , ShouldCancelPending : ptr . Bool ( false ) } ,
{ Installer : installers [ 2 ] } ,
} ,
} ,
{
desc : "team 1, installer0 switch to labels 1 and 3, installer2 no change" ,
team : tm1 ,
payload : [ ] testPayload {
{ Installer : installers [ 0 ] , Labels : [ ] * fleet . Label { labels [ 1 ] , labels [ 3 ] } , ShouldCancelPending : ptr . Bool ( true ) } ,
{ Installer : installers [ 2 ] , ShouldCancelPending : ptr . Bool ( false ) } ,
} ,
} ,
{
desc : "team 2, remove installer0, labels of install1 and no change installer2" ,
team : tm2 ,
payload : [ ] testPayload {
{ Installer : installers [ 1 ] , ShouldCancelPending : ptr . Bool ( true ) } ,
{ Installer : installers [ 2 ] , Labels : [ ] * fleet . Label { labels [ 1 ] , labels [ 2 ] } , Exclude : false , ShouldCancelPending : ptr . Bool ( false ) } ,
} ,
} ,
{
desc : "no team, remove all" ,
payload : [ ] testPayload { } ,
} ,
}
for _ , c := range cases {
t . Log ( "Running test case " , c . desc )
var teamID * uint
var globalOrTeamID uint
if c . team != nil {
teamID = & c . team . ID
globalOrTeamID = c . team . ID
}
// cleanup any existing install requests for the host
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , ` DELETE FROM host_software_installs WHERE host_id = ? ` , host . ID )
return err
} )
installerIDs := make ( [ ] uint , len ( c . payload ) )
if len ( c . payload ) > 0 {
// create pending install requests for each updated installer, to see if
// it cancels it or not as expected.
2025-07-17 14:20:49 +00:00
err := ds . AddHostsToTeam ( ctx , fleet . NewAddHostsToTeamParams ( teamID , [ ] uint { host . ID } ) )
2024-12-17 19:28:17 +00:00
require . NoError ( t , err )
for i , payload := range c . payload {
if payload . ShouldCancelPending != nil {
// the installer must exist
var swID uint
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
err := sqlx . GetContext ( ctx , q , & swID , ` SELECT id FROM software_installers WHERE global_or_team_id = ?
2025-10-07 21:05:22 +00:00
AND title_id IN ( SELECT id FROM software_titles WHERE name = ? AND source = ? AND extension_for = ' ' ) ` ,
2024-12-17 19:28:17 +00:00
globalOrTeamID , payload . Installer . Title , payload . Installer . Source )
return err
} )
2025-02-11 19:53:11 +00:00
_ , err = ds . InsertSoftwareInstallRequest ( ctx , host . ID , swID , fleet . HostSoftwareInstallOptions { } )
2024-12-17 19:28:17 +00:00
require . NoError ( t , err )
installerIDs [ i ] = swID
}
}
}
// create the payload by copying the test one, so that the original installers
// structs are not modified
payload := make ( [ ] * fleet . UploadSoftwareInstallerPayload , len ( c . payload ) )
for i , p := range c . payload {
installer := * p . Installer
installer . ValidatedLabels = & fleet . LabelIdentsWithScope { LabelScope : fleet . LabelScopeIncludeAny }
if p . Exclude {
installer . ValidatedLabels . LabelScope = fleet . LabelScopeExcludeAny
}
byName := make ( map [ string ] fleet . LabelIdent , len ( p . Labels ) )
for _ , lbl := range p . Labels {
byName [ lbl . Name ] = fleet . LabelIdent { LabelName : lbl . Name , LabelID : lbl . ID }
}
installer . ValidatedLabels . ByName = byName
payload [ i ] = & installer
}
err := ds . BatchSetSoftwareInstallers ( ctx , teamID , payload )
require . NoError ( t , err )
installers , err := ds . GetSoftwareInstallers ( ctx , globalOrTeamID )
require . NoError ( t , err )
require . Len ( t , installers , len ( c . payload ) )
// get the metadata for each installer to assert the batch did set the
// expected ones.
installersByFilename := make ( map [ string ] * fleet . SoftwareInstaller , len ( installers ) )
for _ , ins := range installers {
meta , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , teamID , * ins . TitleID , false )
require . NoError ( t , err )
installersByFilename [ meta . Name ] = meta
}
// validate that the inserted software is as expected
for i , payload := range c . payload {
meta , ok := installersByFilename [ payload . Installer . Filename ]
require . True ( t , ok , "installer %s was not created" , payload . Installer . Filename )
require . Equal ( t , meta . SoftwareTitle , payload . Installer . Title )
wantLabelIDs := make ( [ ] uint , len ( payload . Labels ) )
for j , lbl := range payload . Labels {
wantLabelIDs [ j ] = lbl . ID
}
if payload . Exclude {
require . Empty ( t , meta . LabelsIncludeAny )
gotLabelIDs := make ( [ ] uint , len ( meta . LabelsExcludeAny ) )
for i , lbl := range meta . LabelsExcludeAny {
gotLabelIDs [ i ] = lbl . LabelID
}
require . ElementsMatch ( t , wantLabelIDs , gotLabelIDs )
} else {
require . Empty ( t , meta . LabelsExcludeAny )
gotLabelIDs := make ( [ ] uint , len ( meta . LabelsIncludeAny ) )
for j , lbl := range meta . LabelsIncludeAny {
gotLabelIDs [ j ] = lbl . LabelID
}
require . ElementsMatch ( t , wantLabelIDs , gotLabelIDs )
}
// check if it deleted pending installs or not
if payload . ShouldCancelPending != nil {
lastInstall , err := ds . GetHostLastInstallData ( ctx , host . ID , installerIDs [ i ] )
require . NoError ( t , err )
if * payload . ShouldCancelPending {
require . Nil ( t , lastInstall , "should have cancelled pending installs" )
} else {
require . NotNil ( t , lastInstall , "should not have cancelled pending installs" )
}
}
}
}
}
2024-12-27 18:10:28 +00:00
func testMatchOrCreateSoftwareInstallerWithAutomaticPolicies ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
team1 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 1" } )
require . NoError ( t , err )
team2 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 2" } )
require . NoError ( t , err )
tfr1 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
// Test pkg without automatic install doesn't create policy.
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
BundleIdentifier : "com.manual.foobar" ,
Extension : "pkg" ,
StorageID : "storage0" ,
Filename : "foobar0" ,
Title : "Manual foobar" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user1 . ID ,
TeamID : & team1 . ID ,
AutomaticInstall : false ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
team1Policies , _ , err := ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Empty ( t , team1Policies )
// Test pkg.
installerID1 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
BundleIdentifier : "com.foo.bar" ,
Extension : "pkg" ,
StorageID : "storage1" ,
Filename : "foobar1" ,
Title : "Foobar" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user1 . ID ,
TeamID : & team1 . ID ,
AutomaticInstall : true ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
team1Policies , _ , err = ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , team1Policies , 1 )
require . Equal ( t , "[Install software] Foobar (pkg)" , team1Policies [ 0 ] . Name )
require . Equal ( t , "SELECT 1 FROM apps WHERE bundle_identifier = 'com.foo.bar';" , team1Policies [ 0 ] . Query )
require . Equal ( t , "Policy triggers automatic install of Foobar on each host that's missing this software." , team1Policies [ 0 ] . Description )
require . Equal ( t , "darwin" , team1Policies [ 0 ] . Platform )
require . NotNil ( t , team1Policies [ 0 ] . SoftwareInstallerID )
require . Equal ( t , installerID1 , * team1Policies [ 0 ] . SoftwareInstallerID )
require . NotNil ( t , team1Policies [ 0 ] . TeamID )
require . Equal ( t , team1 . ID , * team1Policies [ 0 ] . TeamID )
2025-02-24 21:56:49 +00:00
// Test Mac FMA
fma , err := ds . UpsertMaintainedApp ( ctx , & fleet . MaintainedApp { ID : 1 } )
require . NoError ( t , err )
installerFMA , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2025-03-21 02:21:56 +00:00
InstallerFile : tfr1 ,
BundleIdentifier : "com.foo.fma" ,
Platform : "darwin" ,
Extension : "dmg" ,
FleetMaintainedAppID : ptr . Uint ( fma . ID ) ,
StorageID : "storage1" ,
Filename : "foobar1" ,
Title : "FooFMA" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user1 . ID ,
TeamID : & team1 . ID ,
AutomaticInstall : true ,
AutomaticInstallQuery : "SELECT 1 FROM osquery_info" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
2025-02-24 21:56:49 +00:00
} )
require . NoError ( t , err )
team1Policies , _ , err = ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , team1Policies , 2 )
require . Equal ( t , "[Install software] FooFMA" , team1Policies [ 1 ] . Name )
2025-03-21 02:21:56 +00:00
require . Equal ( t , "SELECT 1 FROM osquery_info" , team1Policies [ 1 ] . Query )
2025-02-24 21:56:49 +00:00
require . Equal ( t , "Policy triggers automatic install of FooFMA on each host that's missing this software." , team1Policies [ 1 ] . Description )
require . Equal ( t , "darwin" , team1Policies [ 1 ] . Platform )
require . NotNil ( t , team1Policies [ 1 ] . SoftwareInstallerID )
require . Equal ( t , installerFMA , * team1Policies [ 1 ] . SoftwareInstallerID )
require . NotNil ( t , team1Policies [ 1 ] . TeamID )
require . Equal ( t , team1 . ID , * team1Policies [ 1 ] . TeamID )
2024-12-27 18:10:28 +00:00
// Test msi.
installerID2 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
Extension : "msi" ,
StorageID : "storage2" ,
Filename : "zoobar1" ,
Title : "Zoobar" ,
Version : "1.0" ,
Source : "programs" ,
UserID : user1 . ID ,
TeamID : nil ,
AutomaticInstall : true ,
PackageIDs : [ ] string { "id1" } ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
2025-07-17 15:33:23 +00:00
// check upgrade code handling
msiPackagesWithNoUpgradeCode , err := ds . GetMSIInstallersWithoutUpgradeCode ( ctx )
require . NoError ( t , err )
require . Equal ( t , map [ uint ] string { installerID2 : "storage2" } , msiPackagesWithNoUpgradeCode )
require . NoError ( t , ds . UpdateInstallerUpgradeCode ( ctx , installerID2 , "upgradecode" ) )
msiPackagesWithNoUpgradeCode , err = ds . GetMSIInstallersWithoutUpgradeCode ( ctx )
require . NoError ( t , err )
require . Empty ( t , msiPackagesWithNoUpgradeCode )
msiThatShouldHaveUpgradeCode , err := ds . GetSoftwareInstallerMetadataByID ( ctx , installerID2 )
require . NoError ( t , err )
require . Equal ( t , "upgradecode" , msiThatShouldHaveUpgradeCode . UpgradeCode )
2024-12-27 18:10:28 +00:00
noTeamPolicies , _ , err := ds . ListTeamPolicies ( ctx , fleet . PolicyNoTeamID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , noTeamPolicies , 1 )
require . Equal ( t , "[Install software] Zoobar (msi)" , noTeamPolicies [ 0 ] . Name )
require . Equal ( t , "SELECT 1 FROM programs WHERE identifying_number = 'id1';" , noTeamPolicies [ 0 ] . Query )
require . Equal ( t , "Policy triggers automatic install of Zoobar on each host that's missing this software." , noTeamPolicies [ 0 ] . Description )
require . Equal ( t , "windows" , noTeamPolicies [ 0 ] . Platform )
require . NotNil ( t , noTeamPolicies [ 0 ] . SoftwareInstallerID )
require . Equal ( t , installerID2 , * noTeamPolicies [ 0 ] . SoftwareInstallerID )
require . NotNil ( t , noTeamPolicies [ 0 ] . TeamID )
require . Equal ( t , fleet . PolicyNoTeamID , * noTeamPolicies [ 0 ] . TeamID )
// Test deb.
installerID3 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
Extension : "deb" ,
StorageID : "storage3" ,
Filename : "barfoo1" ,
Title : "Barfoo" ,
Version : "1.0" ,
Source : "deb_packages" ,
UserID : user1 . ID ,
TeamID : & team2 . ID ,
AutomaticInstall : true ,
PackageIDs : [ ] string { "id1" } ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
team2Policies , _ , err := ds . ListTeamPolicies ( ctx , team2 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , team2Policies , 1 )
require . Equal ( t , "[Install software] Barfoo (deb)" , team2Policies [ 0 ] . Name )
require . Equal ( t , ` SELECT 1 WHERE EXISTS (
SELECT 1 WHERE ( SELECT COUNT ( * ) FROM deb_packages ) = 0
) OR EXISTS (
2025-08-18 22:37:06 +00:00
SELECT 1 FROM deb_packages WHERE name = ' Barfoo ' AND status = ' install ok installed '
2024-12-27 18:10:28 +00:00
) ; ` , team2Policies [ 0 ] . Query )
require . Equal ( t , ` Policy triggers automatic install of Barfoo on each host that ' s missing this software .
Software won ' t be installed on Linux hosts with RPM - based distributions because this policy ' s query is written to always pass on these hosts . ` , team2Policies [ 0 ] . Description )
require . Equal ( t , "linux" , team2Policies [ 0 ] . Platform )
require . NotNil ( t , team2Policies [ 0 ] . SoftwareInstallerID )
require . Equal ( t , installerID3 , * team2Policies [ 0 ] . SoftwareInstallerID )
require . NotNil ( t , team2Policies [ 0 ] . TeamID )
require . Equal ( t , team2 . ID , * team2Policies [ 0 ] . TeamID )
// Test rpm.
installerID4 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
Extension : "rpm" ,
StorageID : "storage4" ,
Filename : "barzoo1" ,
Title : "Barzoo" ,
Version : "1.0" ,
Source : "rpm_packages" ,
UserID : user1 . ID ,
TeamID : & team2 . ID ,
AutomaticInstall : true ,
PackageIDs : [ ] string { "id1" } ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
team2Policies , _ , err = ds . ListTeamPolicies ( ctx , team2 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , team2Policies , 2 )
require . Equal ( t , "[Install software] Barzoo (rpm)" , team2Policies [ 1 ] . Name )
require . Equal ( t , ` SELECT 1 WHERE EXISTS (
SELECT 1 WHERE ( SELECT COUNT ( * ) FROM rpm_packages ) = 0
) OR EXISTS (
SELECT 1 FROM rpm_packages WHERE name = ' Barzoo '
) ; ` , team2Policies [ 1 ] . Query )
require . Equal ( t , ` Policy triggers automatic install of Barzoo on each host that ' s missing this software .
Software won ' t be installed on Linux hosts with Debian - based distributions because this policy ' s query is written to always pass on these hosts . ` , team2Policies [ 1 ] . Description )
require . Equal ( t , "linux" , team2Policies [ 1 ] . Platform )
require . NotNil ( t , team2Policies [ 0 ] . SoftwareInstallerID )
require . Equal ( t , installerID4 , * team2Policies [ 1 ] . SoftwareInstallerID )
require . NotNil ( t , team2Policies [ 1 ] . TeamID )
require . Equal ( t , team2 . ID , * team2Policies [ 1 ] . TeamID )
_ , err = ds . NewTeamPolicy ( ctx , team1 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "[Install software] OtherFoobar (pkg)" ,
Query : "SELECT 1;" ,
} )
require . NoError ( t , err )
// Test pkg and policy with name already exists.
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
BundleIdentifier : "com.foo2.bar2" ,
Extension : "pkg" ,
StorageID : "storage5" ,
Filename : "foobar5" ,
Title : "OtherFoobar" ,
Version : "2.0" ,
Source : "apps" ,
UserID : user1 . ID ,
TeamID : & team1 . ID ,
AutomaticInstall : true ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
team1Policies , _ , err = ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
2025-02-24 21:56:49 +00:00
require . Len ( t , team1Policies , 4 )
require . Equal ( t , "[Install software] OtherFoobar (pkg) 2" , team1Policies [ 3 ] . Name )
2024-12-27 18:10:28 +00:00
team3 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 3" } )
require . NoError ( t , err )
_ , err = ds . NewTeamPolicy ( ctx , team3 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "[Install software] Something2 (msi)" ,
Query : "SELECT 1;" ,
} )
require . NoError ( t , err )
_ , err = ds . NewTeamPolicy ( ctx , team3 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "[Install software] Something2 (msi) 2" ,
Query : "SELECT 1;" ,
} )
require . NoError ( t , err )
// This name is on another team, so it shouldn't count.
_ , err = ds . NewTeamPolicy ( ctx , team1 . ID , & user1 . ID , fleet . PolicyPayload {
Name : "[Install software] Something2 (msi) 3" ,
Query : "SELECT 1;" ,
} )
require . NoError ( t , err )
// Test msi and policy with name already exists.
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
Extension : "msi" ,
StorageID : "storage6" ,
Filename : "foobar6" ,
Title : "Something2" ,
PackageIDs : [ ] string { "id2" } ,
Version : "2.0" ,
Source : "programs" ,
UserID : user1 . ID ,
TeamID : & team3 . ID ,
AutomaticInstall : true ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
team3Policies , _ , err := ds . ListTeamPolicies ( ctx , team3 . ID , fleet . ListOptions { } , fleet . ListOptions { } )
require . NoError ( t , err )
require . Len ( t , team3Policies , 3 )
require . Equal ( t , "[Install software] Something2 (msi) 3" , team3Policies [ 2 ] . Name )
}
2025-02-11 19:53:11 +00:00
2025-06-03 15:09:43 +00:00
func testGetDetailsForUninstallFromExecutionID ( t * testing . T , ds * Datastore ) {
2025-02-11 19:53:11 +00:00
ctx := context . Background ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
host := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
tfr1 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
// create a couple software titles
installer1 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
BundleIdentifier : "foobar0" ,
Extension : "pkg" ,
StorageID : "storage0" ,
Filename : "foobar0" ,
Title : "foobar" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
installer2 , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
BundleIdentifier : "foobar1" ,
Extension : "pkg" ,
StorageID : "storage1" ,
Filename : "foobar1" ,
Title : "barfoo" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
// get software title for unknown exec id
2025-06-03 15:09:43 +00:00
title , selfService , err := ds . GetDetailsForUninstallFromExecutionID ( ctx , "unknown" )
2025-02-11 19:53:11 +00:00
require . ErrorIs ( t , err , sql . ErrNoRows )
require . Empty ( t , title )
2025-06-03 15:09:43 +00:00
require . False ( t , selfService )
2025-02-11 19:53:11 +00:00
// create a couple pending software install request, the first will be
// immediately present in host_software_installs too (activated)
req1 , err := ds . InsertSoftwareInstallRequest ( ctx , host . ID , installer1 , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
req2 , err := ds . InsertSoftwareInstallRequest ( ctx , host . ID , installer2 , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
2025-06-03 15:09:43 +00:00
_ , _ , err = ds . GetDetailsForUninstallFromExecutionID ( ctx , req1 )
require . ErrorIs ( t , err , sql . ErrNoRows )
2025-02-11 19:53:11 +00:00
// record a result for req1, will be deleted from upcoming_activities
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : host . ID ,
InstallUUID : req1 ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
2025-06-03 15:09:43 +00:00
_ , _ , err = ds . GetDetailsForUninstallFromExecutionID ( ctx , req1 )
require . ErrorIs ( t , err , sql . ErrNoRows )
2025-02-11 19:53:11 +00:00
// create an uninstall request for installer1
req3 := uuid . NewString ( )
2025-06-03 15:09:43 +00:00
err = ds . InsertSoftwareUninstallRequest ( ctx , req3 , host . ID , installer1 , true )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
2025-06-03 15:09:43 +00:00
title , selfService , err = ds . GetDetailsForUninstallFromExecutionID ( ctx , req3 )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
require . Equal ( t , "foobar" , title )
2025-06-03 15:09:43 +00:00
require . True ( t , selfService )
2025-02-11 19:53:11 +00:00
// record a result for req2, will activate req3 so it is now in host_software_installs too
2025-04-09 20:08:51 +00:00
_ , err = ds . SetHostSoftwareInstallResult ( ctx , & fleet . HostSoftwareInstallResultPayload {
2025-02-11 19:53:11 +00:00
HostID : host . ID ,
InstallUUID : req2 ,
InstallScriptExitCode : ptr . Int ( 0 ) ,
} )
require . NoError ( t , err )
2025-06-03 15:09:43 +00:00
title , selfService , err = ds . GetDetailsForUninstallFromExecutionID ( ctx , req3 )
2025-02-11 19:53:11 +00:00
require . NoError ( t , err )
require . Equal ( t , "foobar" , title )
2025-06-03 15:09:43 +00:00
require . True ( t , selfService )
2025-02-11 19:53:11 +00:00
}
2025-04-18 20:41:41 +00:00
func testGetTeamsWithInstallerByHash ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
team1 , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team 1" } )
require . NoError ( t , err )
tfr1 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
hash1 , hash2 , hash3 := "hash1" , "hash2" , "hash3"
// Add some software installers to No team
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallerFile : tfr1 ,
BundleIdentifier : "bid1" ,
Extension : "pkg" ,
StorageID : hash1 ,
Filename : "installer1.pkg" ,
Title : "installer1" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
Platform : "darwin" ,
URL : "https://example.com/1" ,
} , {
InstallerFile : tfr1 ,
BundleIdentifier : "bid2" ,
Extension : "pkg" ,
StorageID : hash2 ,
Filename : "installer2.pkg" ,
Title : "installer2" ,
Version : "2.0" ,
Source : "apps" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
Platform : "darwin" ,
URL : "https://example.com/2" ,
} ,
} )
require . NoError ( t , err )
// Add some installers to Team 1
err = ds . BatchSetSoftwareInstallers ( ctx , & team1 . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallerFile : tfr1 ,
BundleIdentifier : "bid1" ,
Extension : "pkg" ,
StorageID : hash1 ,
Filename : "installer1.pkg" ,
Title : "installer1" ,
Version : "1.0" ,
Source : "apps" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
TeamID : & team1 . ID ,
Platform : "darwin" ,
URL : "https://example.com/1" ,
} ,
{
InstallerFile : tfr1 ,
BundleIdentifier : "bid3" ,
Extension : "pkg" ,
StorageID : hash3 ,
Filename : "installer3.pkg" ,
Title : "installer3" ,
Version : "3.0" ,
Source : "apps" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
TeamID : & team1 . ID ,
Platform : "darwin" ,
URL : "https://example.com/4" ,
} ,
} )
require . NoError ( t , err )
2025-11-11 21:38:54 +00:00
// add an in-house app to the team
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
TeamID : & team1 . ID ,
UserID : user . ID ,
Title : "inhouse" ,
Filename : "inhouse.ipa" ,
BundleIdentifier : "com.inhouse" ,
StorageID : "inhouse" ,
Extension : "ipa" ,
Version : "1.2.3" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
2025-04-18 20:41:41 +00:00
// get installer IDs from added installers
var installer1NoTeam , installer1Team1 , installer2NoTeam uint
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
err := sqlx . GetContext ( ctx , q , & installer1NoTeam , "SELECT id FROM software_installers WHERE filename = ? AND global_or_team_id = ?" , "installer1.pkg" , 0 )
require . NoError ( t , err )
require . NotEmpty ( t , installer1NoTeam )
err = sqlx . GetContext ( ctx , q , & installer1Team1 , "SELECT id FROM software_installers WHERE filename = ? AND global_or_team_id = ?" , "installer1.pkg" , team1 . ID )
require . NoError ( t , err )
require . NotEmpty ( t , installer1Team1 )
err = sqlx . GetContext ( ctx , q , & installer2NoTeam , "SELECT id FROM software_installers WHERE filename = ? AND global_or_team_id = ?" , "installer2.pkg" , 0 )
require . NoError ( t , err )
require . NotEmpty ( t , installer2NoTeam )
return nil
} )
// fetching by non-existent hash returns empty map
installers , err := ds . GetTeamsWithInstallerByHash ( ctx , "not_found" , "foobar" )
require . NoError ( t , err )
require . Empty ( t , installers )
// there should be 2 installers, one for No team and one for Team 1
installers , err = ds . GetTeamsWithInstallerByHash ( ctx , hash1 , "https://example.com/1" )
require . NoError ( t , err )
require . Len ( t , installers , 2 )
2025-11-11 21:38:54 +00:00
require . Len ( t , installers [ 0 ] , 1 )
require . Equal ( t , installer1NoTeam , installers [ 0 ] [ 0 ] . InstallerID )
require . Nil ( t , installers [ 0 ] [ 0 ] . TeamID )
2025-04-18 20:41:41 +00:00
2025-11-11 21:38:54 +00:00
require . Len ( t , installers [ 1 ] , 1 )
require . Equal ( t , installer1Team1 , installers [ 1 ] [ 0 ] . InstallerID )
require . NotNil ( t , installers [ 1 ] [ 0 ] . TeamID )
require . Equal ( t , team1 . ID , * installers [ 1 ] [ 0 ] . TeamID )
2025-04-18 20:41:41 +00:00
2025-11-11 21:38:54 +00:00
for _ , is := range installers {
i := is [ 0 ]
2025-04-18 20:41:41 +00:00
require . Equal ( t , "installer1" , i . Title )
require . Equal ( t , "pkg" , i . Extension )
require . Equal ( t , "1.0" , i . Version )
require . Equal ( t , "darwin" , i . Platform )
}
installers , err = ds . GetTeamsWithInstallerByHash ( ctx , hash2 , "https://example.com/2" )
require . NoError ( t , err )
require . Len ( t , installers , 1 )
2025-11-11 21:38:54 +00:00
require . Len ( t , installers [ 0 ] , 1 )
require . Equal ( t , installers [ 0 ] [ 0 ] . InstallerID , installer2NoTeam )
// in-house hash with invalid url
installers , err = ds . GetTeamsWithInstallerByHash ( ctx , "inhouse" , "https://no-such-match" )
require . NoError ( t , err )
require . Len ( t , installers , 0 )
// in-house hash without url match
installers , err = ds . GetTeamsWithInstallerByHash ( ctx , "inhouse" , "" )
require . NoError ( t , err )
require . Len ( t , installers , 1 )
require . Len ( t , installers [ team1 . ID ] , 2 ) // ios and ipados
require . Equal ( t , "inhouse.ipa" , installers [ team1 . ID ] [ 0 ] . Filename )
require . Equal ( t , "inhouse.ipa" , installers [ team1 . ID ] [ 1 ] . Filename )
var foundPlatforms [ ] string
for _ , inst := range installers [ team1 . ID ] {
foundPlatforms = append ( foundPlatforms , inst . Platform )
}
require . ElementsMatch ( t , [ ] string { "ios" , "ipados" } , foundPlatforms )
2025-04-18 20:41:41 +00:00
}
2025-05-27 20:08:08 +00:00
func testEditDeleteSoftwareInstallersActivateNextActivity ( t * testing . T , ds * Datastore ) {
ctx := t . Context ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
// create a few installers
newInstallerFile := func ( ident string ) * fleet . TempFileReader {
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( ident ) , t . TempDir )
require . NoError ( t , err )
return tfr
}
err := ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer1" ) ,
StorageID : "installer1" ,
Filename : "installer1" ,
Title : "installer1" ,
Source : "apps" ,
Version : "1" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/1" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer2" ) ,
StorageID : "installer2" ,
Filename : "installer2" ,
Title : "installer2" ,
Source : "apps" ,
Version : "2" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/2" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
installers , err := ds . GetSoftwareInstallers ( ctx , 0 )
require . NoError ( t , err )
require . Len ( t , installers , 2 )
sort . Slice ( installers , func ( i , j int ) bool {
return installers [ i ] . URL < installers [ j ] . URL
} )
ins1 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , nil , * installers [ 0 ] . TitleID , false )
require . NoError ( t , err )
ins2 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , nil , * installers [ 1 ] . TitleID , false )
require . NoError ( t , err )
// create a few hosts
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
host2 := test . NewHost ( t , ds , "host2" , "2" , "host2key" , "host2uuid" , time . Now ( ) )
host3 := test . NewHost ( t , ds , "host3" , "3" , "host3key" , "host3uuid" , time . Now ( ) )
// enqueue software installs on each host
host1Ins1 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , ins1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host1Ins2 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , ins2 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
// add a script exec as last activity for host1
host1Script , err := ds . NewHostScriptExecutionRequest ( ctx , & fleet . HostScriptRequestPayload {
HostID : host1 . ID , ScriptContents : "echo" , UserID : & user . ID , SyncRequest : true ,
} )
require . NoError ( t , err )
host2Ins1 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , ins1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host2Ins2 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , ins2 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
// add a script exec as first activity for host3
host3Script , err := ds . NewHostScriptExecutionRequest ( ctx , & fleet . HostScriptRequestPayload {
HostID : host3 . ID , ScriptContents : "echo" , UserID : & user . ID , SyncRequest : true ,
} )
require . NoError ( t , err )
host3Ins2 , err := ds . InsertSoftwareInstallRequest ( ctx , host3 . ID , ins2 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
checkUpcomingActivities ( t , ds , host1 , host1Ins1 , host1Ins2 , host1Script . ExecutionID )
checkUpcomingActivities ( t , ds , host2 , host2Ins1 , host2Ins2 )
checkUpcomingActivities ( t , ds , host3 , host3Script . ExecutionID , host3Ins2 )
// simulate an update to installer 1 metadata
err = ds . ProcessInstallerUpdateSideEffects ( ctx , ins1 . InstallerID , true , false )
require . NoError ( t , err )
// installer 1 activities were deleted, next activity was activated
checkUpcomingActivities ( t , ds , host1 , host1Ins2 , host1Script . ExecutionID )
checkUpcomingActivities ( t , ds , host2 , host2Ins2 )
checkUpcomingActivities ( t , ds , host3 , host3Script . ExecutionID , host3Ins2 )
// delete installer 2
err = ds . DeleteSoftwareInstaller ( ctx , ins2 . InstallerID )
require . NoError ( t , err )
// installer 2 activities were deleted, next activity was activated for host1 and host2
checkUpcomingActivities ( t , ds , host1 , host1Script . ExecutionID )
checkUpcomingActivities ( t , ds , host2 )
checkUpcomingActivities ( t , ds , host3 , host3Script . ExecutionID )
}
func testBatchSetSoftwareInstallersActivateNextActivity ( t * testing . T , ds * Datastore ) {
ctx := t . Context ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
// create a few installers
newInstallerFile := func ( ident string ) * fleet . TempFileReader {
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( ident ) , t . TempDir )
require . NoError ( t , err )
return tfr
}
err := ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer1" ) ,
StorageID : "installer1" ,
Filename : "installer1" ,
Title : "installer1" ,
Source : "apps" ,
Version : "1" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/1" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer2" ) ,
StorageID : "installer2" ,
Filename : "installer2" ,
Title : "installer2" ,
Source : "apps" ,
Version : "2" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/2" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer3" ) ,
StorageID : "installer3" ,
Filename : "installer3" ,
Title : "installer3" ,
Source : "apps" ,
Version : "3" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/3" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
installers , err := ds . GetSoftwareInstallers ( ctx , 0 )
require . NoError ( t , err )
require . Len ( t , installers , 3 )
sort . Slice ( installers , func ( i , j int ) bool {
return installers [ i ] . URL < installers [ j ] . URL
} )
ins1 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , nil , * installers [ 0 ] . TitleID , false )
require . NoError ( t , err )
ins2 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , nil , * installers [ 1 ] . TitleID , false )
require . NoError ( t , err )
ins3 , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , nil , * installers [ 2 ] . TitleID , false )
require . NoError ( t , err )
// create a few hosts
host1 := test . NewHost ( t , ds , "host1" , "1" , "host1key" , "host1uuid" , time . Now ( ) )
host2 := test . NewHost ( t , ds , "host2" , "2" , "host2key" , "host2uuid" , time . Now ( ) )
host3 := test . NewHost ( t , ds , "host3" , "3" , "host3key" , "host3uuid" , time . Now ( ) )
// enqueue software installs on each host
host1Ins1 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , ins1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host1Ins2 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , ins2 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host1Ins3 , err := ds . InsertSoftwareInstallRequest ( ctx , host1 . ID , ins3 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host2Ins2 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , ins2 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host2Ins1 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , ins1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host2Ins3 , err := ds . InsertSoftwareInstallRequest ( ctx , host2 . ID , ins3 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host3Ins3 , err := ds . InsertSoftwareInstallRequest ( ctx , host3 . ID , ins3 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host3Ins2 , err := ds . InsertSoftwareInstallRequest ( ctx , host3 . ID , ins2 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
host3Ins1 , err := ds . InsertSoftwareInstallRequest ( ctx , host3 . ID , ins1 . InstallerID , fleet . HostSoftwareInstallOptions { } )
require . NoError ( t , err )
checkUpcomingActivities ( t , ds , host1 , host1Ins1 , host1Ins2 , host1Ins3 )
checkUpcomingActivities ( t , ds , host2 , host2Ins2 , host2Ins1 , host2Ins3 )
checkUpcomingActivities ( t , ds , host3 , host3Ins3 , host3Ins2 , host3Ins1 )
// no change
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer1" ) ,
StorageID : "installer1" ,
Filename : "installer1" ,
Title : "installer1" ,
Source : "apps" ,
Version : "1" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/1" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer2" ) ,
StorageID : "installer2" ,
Filename : "installer2" ,
Title : "installer2" ,
Source : "apps" ,
Version : "2" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/2" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer3" ) ,
StorageID : "installer3" ,
Filename : "installer3" ,
Title : "installer3" ,
Source : "apps" ,
Version : "3" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/3" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
checkUpcomingActivities ( t , ds , host1 , host1Ins1 , host1Ins2 , host1Ins3 )
checkUpcomingActivities ( t , ds , host2 , host2Ins2 , host2Ins1 , host2Ins3 )
checkUpcomingActivities ( t , ds , host3 , host3Ins3 , host3Ins2 , host3Ins1 )
// remove installer 1, update installer 2
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer2" ) ,
PreInstallQuery : "SELECT 1" , // <- metadata updated
StorageID : "installer2" ,
Filename : "installer2" ,
Title : "installer2" ,
Source : "apps" ,
Version : "2" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/2" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
{
InstallScript : "install" ,
InstallerFile : newInstallerFile ( "installer3" ) ,
StorageID : "installer3" ,
Filename : "installer3" ,
Title : "installer3" ,
Source : "apps" ,
Version : "3" ,
UserID : user . ID ,
Platform : "darwin" ,
URL : "https://example.com/3" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
// installer 1 and 2 activities were deleted, next activity was activated
checkUpcomingActivities ( t , ds , host1 , host1Ins3 )
checkUpcomingActivities ( t , ds , host2 , host2Ins3 )
checkUpcomingActivities ( t , ds , host3 , host3Ins3 )
// add a pending script on host 1 and 2
host1Script , err := ds . NewHostScriptExecutionRequest ( ctx , & fleet . HostScriptRequestPayload {
HostID : host1 . ID , ScriptContents : "echo" , UserID : & user . ID , SyncRequest : true ,
} )
require . NoError ( t , err )
host2Script , err := ds . NewHostScriptExecutionRequest ( ctx , & fleet . HostScriptRequestPayload {
HostID : host2 . ID , ScriptContents : "echo" , UserID : & user . ID , SyncRequest : true ,
} )
require . NoError ( t , err )
// clear everything
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload { } )
require . NoError ( t , err )
checkUpcomingActivities ( t , ds , host1 , host1Script . ExecutionID )
checkUpcomingActivities ( t , ds , host2 , host2Script . ExecutionID )
checkUpcomingActivities ( t , ds , host3 )
}
2025-06-13 15:36:10 +00:00
func testSaveInstallerUpdatesClearsFleetMaintainedAppID ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
user := test . NewUser ( t , ds , "Test User" , "test@example.com" , true )
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( "file contents" ) , t . TempDir )
require . NoError ( t , err )
// Create a maintained app
maintainedApp , err := ds . UpsertMaintainedApp ( ctx , & fleet . MaintainedApp {
Name : "Maintained1" ,
Slug : "maintained1" ,
Platform : "darwin" ,
UniqueIdentifier : "fleet.maintained1" ,
} )
require . NoError ( t , err )
// Create an installer with a non-NULL fleet_maintained_app_id
2025-11-04 15:04:42 +00:00
installerID , titleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2025-06-13 15:36:10 +00:00
Title : "testpkg" ,
Source : "apps" ,
InstallScript : "echo install" ,
PreInstallQuery : "SELECT 1" ,
UninstallScript : "echo uninstall" ,
InstallerFile : tfr ,
StorageID : "storageid1" ,
Filename : "test.pkg" ,
Version : "1.0" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
FleetMaintainedAppID : ptr . Uint ( maintainedApp . ID ) ,
} )
require . NoError ( t , err )
// Prepare update payload with a new installer file (should clear FMA id)
installScript := "echo install updated"
uninstallScript := "echo uninstall updated"
preInstallQuery := "SELECT 2"
selfService := true
payload := & fleet . UpdateSoftwareInstallerPayload {
2025-11-04 15:04:42 +00:00
TitleID : titleID ,
2025-06-13 15:36:10 +00:00
InstallerID : installerID ,
StorageID : "storageid2" , // different storage id
Filename : "test2.pkg" ,
Version : "2.0" ,
PackageIDs : [ ] string { "com.test.pkg" } ,
InstallScript : & installScript ,
UninstallScript : & uninstallScript ,
PreInstallQuery : & preInstallQuery ,
SelfService : & selfService ,
InstallerFile : tfr , // triggers clearing
UserID : user . ID ,
}
require . NoError ( t , ds . SaveInstallerUpdates ( ctx , payload ) )
// Assert that fleet_maintained_app_id is now NULL
var fmaID * uint
err = sqlx . GetContext ( ctx , ds . reader ( ctx ) , & fmaID , ` SELECT fleet_maintained_app_id FROM software_installers WHERE id = ? ` , installerID )
require . NoError ( t , err )
assert . Nil ( t , fmaID , "fleet_maintained_app_id should be NULL after update" )
}
2025-10-14 20:59:01 +00:00
func testSoftwareInstallerReplicaLag ( t * testing . T , _ * Datastore ) {
opts := & testing_utils . DatastoreTestOptions { DummyReplica : true }
ds := CreateMySQLDSWithOptions ( t , opts )
defer ds . Close ( )
ctx := context . Background ( )
test . NewHost ( t , ds , "host1" , "" , "host1key" , "host1uuid" , time . Now ( ) )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "Team 1" } )
require . NoError ( t , err )
opts . RunReplication ( )
// upload software installer
installerID , titleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
Title : "foo" ,
Source : "apps" ,
Version : "1.0" ,
InstallScript : "echo" ,
StorageID : "storage" ,
Filename : "installer.pkg" ,
BundleIdentifier : "com.foo.installer" ,
UserID : user . ID ,
TeamID : & team . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
require . NotZero ( t , installerID )
require . NotZero ( t , titleID )
// opts.RunReplication() // - replication should not be needed after fix
ctx = ctxdb . RequirePrimary ( ctx , true )
// then validate it GetSoftwareInstallerMetadataByTeamAndTitleID()
gotInstaller , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , & team . ID , titleID , false )
require . NoError ( t , err )
require . NotNil ( t , gotInstaller )
}
2025-11-04 15:04:42 +00:00
func testSoftwareTitleDisplayName ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
tfr1 , err := fleet . NewTempFileReader ( strings . NewReader ( "hello" ) , t . TempDir )
require . NoError ( t , err )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
host0 := test . NewHost ( t , ds , "host0" , "" , "host0key" , "host0uuid" , time . Now ( ) )
2025-11-26 16:17:03 +00:00
installerID , titleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
2025-11-04 15:04:42 +00:00
InstallerFile : tfr1 ,
Extension : "msi" ,
StorageID : "storageid" ,
Filename : "originalname.msi" ,
Title : "OriginalName1" ,
PackageIDs : [ ] string { "id2" } ,
Version : "2.0" ,
Source : "programs" ,
AutomaticInstall : true ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
// Display name is empty by default
titles , _ , _ , err := ds . ListSoftwareTitles (
ctx ,
fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( 0 ) } ,
fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } ,
)
require . NoError ( t , err )
assert . Len ( t , titles , 1 )
assert . Empty ( t , titles [ 0 ] . DisplayName )
title , err := ds . SoftwareTitleByID ( ctx , titleID , ptr . Uint ( 0 ) , fleet . TeamFilter { } )
require . NoError ( t , err )
assert . Empty ( t , title . DisplayName )
err = ds . SaveInstallerUpdates ( ctx , & fleet . UpdateSoftwareInstallerPayload {
2025-11-19 00:23:18 +00:00
DisplayName : ptr . String ( "update1" ) ,
2025-11-04 15:04:42 +00:00
TitleID : titleID ,
InstallerFile : & fleet . TempFileReader { } ,
InstallScript : new ( string ) ,
PreInstallQuery : new ( string ) ,
PostInstallScript : new ( string ) ,
SelfService : ptr . Bool ( false ) ,
UninstallScript : new ( string ) ,
} )
require . NoError ( t , err )
// Display name entry should be in join table
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
type result struct {
DisplayName string ` db:"display_name" `
SoftwareTitleID uint ` db:"software_title_id" `
TeamID uint ` db:"team_id" `
}
var r [ ] result
err := sqlx . SelectContext ( ctx , q , & r , "SELECT display_name, software_title_id, team_id FROM software_title_display_names" )
require . NoError ( t , err )
assert . Len ( t , r , 1 )
assert . Equal ( t , r [ 0 ] , result { "update1" , titleID , 0 } )
return nil
} )
// List contains display name
titles , _ , _ , err = ds . ListSoftwareTitles (
ctx ,
fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( 0 ) } ,
fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } ,
)
require . NoError ( t , err )
assert . Len ( t , titles , 1 )
assert . Equal ( t , "update1" , titles [ 0 ] . DisplayName )
// Entity contains display name
title , err = ds . SoftwareTitleByID ( ctx , titleID , ptr . Uint ( 0 ) , fleet . TeamFilter { } )
require . NoError ( t , err )
assert . Equal ( t , "update1" , title . DisplayName )
// Update host's software so we get a software version
software0 := [ ] fleet . Software {
{ Name : "OriginalName1" , Version : "0.0.1" , Source : "programs" , TitleID : ptr . Uint ( titleID ) } ,
}
_ , err = ds . UpdateHostSoftware ( ctx , host0 . ID , software0 )
require . NoError ( t , err )
require . NoError ( t , ds . SyncHostsSoftware ( ctx , time . Now ( ) ) )
softwareList , _ , err := ds . ListSoftware ( ctx , fleet . SoftwareListOptions { } )
require . NoError ( t , err )
assert . Len ( t , softwareList , 1 )
assert . Equal ( t , titleID , * softwareList [ 0 ] . TitleID )
assert . Equal ( t , "update1" , softwareList [ 0 ] . DisplayName )
software , err := ds . SoftwareByID ( ctx , softwareList [ 0 ] . ID , ptr . Uint ( 0 ) , false , & fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } )
require . NoError ( t , err )
assert . Equal ( t , titleID , * software . TitleID )
assert . Equal ( t , "update1" , software . DisplayName )
// Update the display name again, should see the change
err = ds . SaveInstallerUpdates ( ctx , & fleet . UpdateSoftwareInstallerPayload {
2025-11-19 00:23:18 +00:00
DisplayName : ptr . String ( "update2" ) ,
2025-11-04 15:04:42 +00:00
TitleID : titleID ,
InstallerFile : & fleet . TempFileReader { } ,
InstallScript : new ( string ) ,
PreInstallQuery : new ( string ) ,
PostInstallScript : new ( string ) ,
SelfService : ptr . Bool ( false ) ,
UninstallScript : new ( string ) ,
} )
require . NoError ( t , err )
// List contains display name
titles , _ , _ , err = ds . ListSoftwareTitles (
ctx ,
fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( 0 ) } ,
fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } ,
)
require . NoError ( t , err )
assert . Len ( t , titles , 1 )
assert . Equal ( t , "update2" , titles [ 0 ] . DisplayName )
// Entity contains display name
title , err = ds . SoftwareTitleByID ( ctx , titleID , ptr . Uint ( 0 ) , fleet . TeamFilter { } )
require . NoError ( t , err )
assert . Equal ( t , "update2" , title . DisplayName )
softwareList , _ , err = ds . ListSoftware ( ctx , fleet . SoftwareListOptions { } )
require . NoError ( t , err )
assert . Len ( t , softwareList , 1 )
assert . Equal ( t , titleID , * softwareList [ 0 ] . TitleID )
assert . Equal ( t , "update2" , softwareList [ 0 ] . DisplayName )
software , err = ds . SoftwareByID ( ctx , softwareList [ 0 ] . ID , ptr . Uint ( 0 ) , false , & fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } )
require . NoError ( t , err )
assert . Equal ( t , titleID , * software . TitleID )
assert . Equal ( t , "update2" , software . DisplayName )
// Update display name to be empty
err = ds . SaveInstallerUpdates ( ctx , & fleet . UpdateSoftwareInstallerPayload {
TitleID : titleID ,
InstallerFile : & fleet . TempFileReader { } ,
InstallScript : new ( string ) ,
PreInstallQuery : new ( string ) ,
PostInstallScript : new ( string ) ,
SelfService : ptr . Bool ( false ) ,
UninstallScript : new ( string ) ,
2025-11-19 00:23:18 +00:00
DisplayName : ptr . String ( "" ) ,
2025-11-04 15:04:42 +00:00
} )
require . NoError ( t , err )
// List contains display name
titles , _ , _ , err = ds . ListSoftwareTitles (
ctx ,
fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( 0 ) } ,
fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } ,
)
require . NoError ( t , err )
assert . Len ( t , titles , 1 )
assert . Empty ( t , titles [ 0 ] . DisplayName )
// Entity contains display name
title , err = ds . SoftwareTitleByID ( ctx , titleID , ptr . Uint ( 0 ) , fleet . TeamFilter { } )
require . NoError ( t , err )
assert . Empty ( t , title . DisplayName )
softwareList , _ , err = ds . ListSoftware ( ctx , fleet . SoftwareListOptions { } )
require . NoError ( t , err )
assert . Len ( t , softwareList , 1 )
assert . Equal ( t , titleID , * softwareList [ 0 ] . TitleID )
assert . Empty ( t , softwareList [ 0 ] . DisplayName )
software , err = ds . SoftwareByID ( ctx , softwareList [ 0 ] . ID , ptr . Uint ( 0 ) , false , & fleet . TeamFilter { User : & fleet . User {
GlobalRole : ptr . String ( fleet . RoleAdmin ) ,
} } )
require . NoError ( t , err )
assert . Equal ( t , titleID , * software . TitleID )
assert . Empty ( t , software . DisplayName )
2025-11-26 16:17:03 +00:00
// Delete software installer, display name should be deleted
_ , err = ds . DeleteTeamPolicies ( ctx , 0 , [ ] uint { 1 } )
require . NoError ( t , err )
require . NoError ( t , ds . DeleteSoftwareInstaller ( ctx , installerID ) )
_ , err = ds . getSoftwareTitleDisplayName ( ctx , 0 , titleID )
require . ErrorContains ( t , err , "not found" )
2025-12-08 15:01:07 +00:00
// Add installer, vpp, in-house app with custom names
_ , titleID , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr1 ,
Extension : "msi" ,
StorageID : "storageid" ,
Filename : "originalname.msi" ,
Title : "OriginalName1" ,
PackageIDs : [ ] string { "id2" } ,
Version : "2.0" ,
Source : "programs" ,
AutomaticInstall : true ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
err = ds . SaveInstallerUpdates ( ctx , & fleet . UpdateSoftwareInstallerPayload {
DisplayName : ptr . String ( "update2" ) ,
TitleID : titleID ,
InstallerFile : & fleet . TempFileReader { } ,
InstallScript : new ( string ) ,
PreInstallQuery : new ( string ) ,
PostInstallScript : new ( string ) ,
SelfService : ptr . Bool ( false ) ,
UninstallScript : new ( string ) ,
} )
require . NoError ( t , err )
payload := fleet . UploadSoftwareInstallerPayload {
UserID : user1 . ID ,
Title : "foo" ,
BundleIdentifier : "com.foo" ,
Filename : "foo.ipa" ,
StorageID : "testingtesting123" ,
Platform : "ios" ,
Extension : "ipa" ,
Version : "1.2.3" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
}
ipaInstallerID , ipaTitleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & payload )
require . NoError ( t , err )
err = ds . SaveInHouseAppUpdates ( ctx , & fleet . UpdateSoftwareInstallerPayload {
TitleID : ipaTitleID ,
InstallerID : ipaInstallerID ,
DisplayName : ptr . String ( "ipa_foo" ) ,
} )
require . NoError ( t , err )
test . CreateInsertGlobalVPPToken ( t , ds )
_ , err = ds . InsertVPPAppWithTeam ( ctx , & fleet . VPPApp {
VPPAppTeam : fleet . VPPAppTeam { VPPAppID : fleet . VPPAppID { AdamID : "adam_vpp_1" , Platform : "darwin" } , DisplayName : ptr . String ( "VPP1" ) } ,
Name : "vpp1" ,
BundleIdentifier : "com.app.vpp1" ,
} , nil )
require . NoError ( t , err )
// Batch insert installers should delete previous display names
// and ignore in-house and vpp names
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install" ,
InstallerFile : & fleet . TempFileReader { } ,
StorageID : "storageid" ,
Filename : "originalname.msi" ,
Title : "OriginalName1" ,
DisplayName : "batch_name1" ,
Source : "apps" ,
Version : "1" ,
PreInstallQuery : "select 0 from foo;" ,
UserID : user1 . ID ,
Platform : "darwin" ,
URL : "https://example.com" ,
InstallDuringSetup : ptr . Bool ( true ) ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} ,
} )
require . NoError ( t , err )
getAllDisplayNames := func ( ) [ ] string {
var names [ ] string
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
err := sqlx . SelectContext ( ctx , q , & names , ` SELECT display_name FROM software_title_display_names ` )
require . NoError ( t , err )
return nil
} )
return names
}
names := getAllDisplayNames ( )
require . Len ( t , names , 3 )
require . NotContains ( t , names , "update2" )
require . Contains ( t , names , "batch_name1" )
require . Contains ( t , names , "VPP1" )
require . Contains ( t , names , "ipa_foo" )
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload { } )
require . NoError ( t , err )
names = getAllDisplayNames ( )
require . Len ( t , names , 2 )
require . Contains ( t , names , "VPP1" )
require . Contains ( t , names , "ipa_foo" )
2025-11-04 15:04:42 +00:00
}
2025-11-05 16:40:44 +00:00
func testMatchOrCreateSoftwareInstallerDuplicateHash ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
teamA , err := ds . NewTeam ( ctx , & fleet . Team { Name : "Team A" } )
require . NoError ( t , err )
teamB , err := ds . NewTeam ( ctx , & fleet . Team { Name : "Team B" } )
require . NoError ( t , err )
const sameHash = "dup-hash-001"
mkPayload := func ( teamID * uint , filename , title string ) * fleet . UploadSoftwareInstallerPayload {
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( "same-bytes" ) , t . TempDir )
require . NoError ( t , err )
return & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr ,
Extension : "sh" ,
StorageID : sameHash ,
Filename : filename ,
Title : title ,
Version : "1.0" ,
Source : "apps" ,
Platform : "darwin" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
TeamID : teamID ,
}
}
// Create on Team A → success
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPayload ( & teamA . ID , "a.sh" , "title-a" ) )
require . NoError ( t , err )
// Duplicate on Team A with different name/title but same hash → reject
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPayload ( & teamA . ID , "b.sh" , "title-b" ) )
require . Error ( t , err )
var iae * fleet . InvalidArgumentError
if ! errors . As ( err , & iae ) {
t . Fatalf ( "expected InvalidArgumentError for same-team duplicate hash, got: %T: %v" , err , err )
}
// Same hash on different team → allowed
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPayload ( & teamB . ID , "c.sh" , "title-c" ) )
require . NoError ( t , err )
// Global scope first time → allowed
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPayload ( nil , "global1.sh" , "title-g1" ) )
require . NoError ( t , err )
// Global scope second time (duplicate hash) → reject
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPayload ( nil , "global2.sh" , "title-g2" ) )
require . Error ( t , err )
var iae2 * fleet . InvalidArgumentError
if ! errors . As ( err , & iae2 ) {
t . Fatalf ( "expected InvalidArgumentError for global duplicate hash, got: %T: %v" , err , err )
}
// Test that binary packages (.pkg) with duplicate hash ARE allowed
mkPkgPayload := func ( teamID * uint , filename , title string ) * fleet . UploadSoftwareInstallerPayload {
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( "same-binary-bytes" ) , t . TempDir )
require . NoError ( t , err )
return & fleet . UploadSoftwareInstallerPayload {
InstallerFile : tfr ,
Extension : "pkg" ,
StorageID : "same-pkg-hash" ,
Filename : filename ,
Title : title ,
Version : "1.0" ,
Source : "apps" ,
Platform : "darwin" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
TeamID : teamID ,
}
}
// Binary packages with same hash on same team → allowed
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPkgPayload ( & teamA . ID , "pkg1.pkg" , "title-pkg1" ) )
require . NoError ( t , err )
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPkgPayload ( & teamA . ID , "pkg2.pkg" , "title-pkg2" ) )
require . NoError ( t , err , "binary packages with same hash should be allowed on same team" )
2025-12-09 13:48:10 +00:00
// Binary packages with same title on same team → reject
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , mkPayload ( & teamA . ID , "a.sh" , "title-a" ) )
require . ErrorContainsf ( t , err , ` "title-a" already exists with team "Team A". ` , "expected existsError for same-team duplicate title, got: %T: %v" , err , err )
2025-11-05 16:40:44 +00:00
}