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"
2024-05-02 21:00:06 +00:00
"github.com/fleetdm/fleet/v4/server/fleet"
2026-01-08 19:17:19 +00:00
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/platform/mysql/testing_utils"
2024-05-02 21:00:06 +00:00
"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 } ,
2026-01-01 16:32:28 +00:00
{ "BatchSetSoftwareInstallersWithUpgradeCodes" , testBatchSetSoftwareInstallersWithUpgradeCodes } ,
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-10-14 20:59:01 +00:00
{ "SoftwareInstallerReplicaLag" , testSoftwareInstallerReplicaLag } ,
2025-11-04 15:04:42 +00:00
{ "SoftwareTitleDisplayName" , testSoftwareTitleDisplayName } ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
{ "AddSoftwareTitleToMatchingSoftware" , testAddSoftwareTitleToMatchingSoftware } ,
2026-02-26 20:27:16 +00:00
{ "FleetMaintainedAppInstallerUpdates" , testFleetMaintainedAppInstallerUpdates } ,
Repoint policies before deleting installers (#41362)
When removing old installer rows, update policies.software_installer_id
to reference the new/active installer first to avoid FK constraint
failures (there is no ON DELETE CASCADE). For custom installers, repoint
policies that reference older versions before deleting them. For
fleet-maintained apps, collect keep IDs once, build the UPDATE via
sqlx.In to re-point policies that reference evicted versions to the
active installer, then delete the evicted rows. Adds error context for
query construction and execution failures.
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
For unreleased bug fixes in a release candidate, one of:
- [x] Confirmed that the fix is not expected to adversely impact load
test results
---------
Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
2026-03-10 22:19:02 +00:00
{ "RepointCustomPackagePolicyToNewInstaller" , testRepointPolicyToNewInstaller } ,
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
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
2026-01-12 23:30:51 +00:00
} , nil )
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" ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2024-05-29 15:01:48 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
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 ,
2026-01-12 23:30:51 +00:00
} , nil )
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 ,
2026-01-12 23:30:51 +00:00
} , nil )
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 ,
2026-01-12 23:30:51 +00:00
} , nil )
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2025-02-11 19:53:11 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2025-02-11 19:53:11 +00:00
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
}
2026-01-01 16:32:28 +00:00
func testBatchSetSoftwareInstallersWithUpgradeCodes ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
// create a team
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : t . Name ( ) } )
require . NoError ( t , err )
user1 := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
// helper to get upgrade_code from software_titles table
getUpgradeCodeForTitle := func ( titleID uint ) * string {
var upgradeCode * string
err := sqlx . GetContext ( ctx , ds . reader ( ctx ) , & upgradeCode ,
` SELECT upgrade_code FROM software_titles WHERE id = ? ` , titleID )
require . NoError ( t , err )
return upgradeCode
}
// Create a Windows installer with an upgrade code
ins0 := "windows-installer"
ins0File := bytes . NewReader ( [ ] byte ( "installer0" ) )
tfr0 , err := fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
upgradeCode := "{12345678-1234-1234-1234-123456789012}"
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { {
InstallScript : "install.ps1" ,
InstallerFile : tfr0 ,
StorageID : ins0 ,
Filename : "installer0.msi" ,
Title : "Windows App" ,
Source : "programs" ,
Version : "1.0" ,
UserID : user1 . ID ,
Platform : "windows" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
UpgradeCode : upgradeCode ,
} } )
require . NoError ( t , err )
softwareInstallers , err := ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 1 )
require . NotNil ( t , softwareInstallers [ 0 ] . TitleID )
titleID := * softwareInstallers [ 0 ] . TitleID
// Verify the upgrade_code was stored in software_titles
storedUpgradeCode := getUpgradeCodeForTitle ( titleID )
require . NotNil ( t , storedUpgradeCode )
require . Equal ( t , upgradeCode , * storedUpgradeCode )
// Update the installer (same upgrade_code, different version) - should match the same title
ins0File = bytes . NewReader ( [ ] byte ( "installer0-v2" ) )
tfr0 , err = fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { {
InstallScript : "install.ps1" ,
InstallerFile : tfr0 ,
StorageID : ins0 + "-v2" ,
Filename : "installer0-v2.msi" ,
Title : "Windows App" ,
Source : "programs" ,
Version : "2.0" ,
UserID : user1 . ID ,
Platform : "windows" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
UpgradeCode : upgradeCode ,
} } )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 1 )
require . NotNil ( t , softwareInstallers [ 0 ] . TitleID )
// Title ID should be the same since upgrade_code matches
require . Equal ( t , titleID , * softwareInstallers [ 0 ] . TitleID )
// Verify upgrade_code is still correct
storedUpgradeCode = getUpgradeCodeForTitle ( titleID )
require . NotNil ( t , storedUpgradeCode )
require . Equal ( t , upgradeCode , * storedUpgradeCode )
// Add a second Windows installer with no upgrade code
ins1 := "windows-installer2"
ins1File := bytes . NewReader ( [ ] byte ( "installer1" ) )
tfr1 , err := fleet . NewTempFileReader ( ins1File , t . TempDir )
require . NoError ( t , err )
// Reset tfr0 for reuse
ins0File = bytes . NewReader ( [ ] byte ( "installer0-v2" ) )
tfr0 , err = fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload {
{
InstallScript : "install.ps1" ,
InstallerFile : tfr0 ,
StorageID : ins0 + "-v2" ,
Filename : "installer0-v2.msi" ,
Title : "Windows App" ,
Source : "programs" ,
Version : "2.0" ,
UserID : user1 . ID ,
Platform : "windows" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
UpgradeCode : upgradeCode ,
} ,
{
InstallScript : "install2.ps1" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : "installer1.msi" ,
Title : "Another Windows App" ,
Source : "programs" ,
Version : "1.0" ,
UserID : user1 . ID ,
Platform : "windows" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
UpgradeCode : "" ,
} ,
} )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 2 )
// Find the second installer and verify its upgrade_code
var secondTitleID uint
for _ , si := range softwareInstallers {
if * si . TitleID != titleID {
secondTitleID = * si . TitleID
break
}
}
require . NotZero ( t , secondTitleID )
storedUpgradeCode2 := getUpgradeCodeForTitle ( secondTitleID )
require . NotNil ( t , storedUpgradeCode2 )
require . Empty ( t , * storedUpgradeCode2 )
// Verify non-Windows installers don't get upgrade_code set
ins2 := "mac-installer"
ins2File := bytes . NewReader ( [ ] byte ( "installer2" ) )
tfr2 , err := fleet . NewTempFileReader ( ins2File , t . TempDir )
require . NoError ( t , err )
// Reset tfr0 and tfr1 for reuse
ins0File = bytes . NewReader ( [ ] byte ( "installer0-v2" ) )
tfr0 , err = fleet . NewTempFileReader ( ins0File , t . TempDir )
require . NoError ( t , err )
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.ps1" ,
InstallerFile : tfr0 ,
StorageID : ins0 + "-v2" ,
Filename : "installer0-v2.msi" ,
Title : "Windows App" ,
Source : "programs" ,
Version : "2.0" ,
UserID : user1 . ID ,
Platform : "windows" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
UpgradeCode : upgradeCode ,
} ,
{
InstallScript : "install2.ps1" ,
InstallerFile : tfr1 ,
StorageID : ins1 ,
Filename : "installer1.msi" ,
Title : "Another Windows App" ,
Source : "programs" ,
Version : "1.0" ,
UserID : user1 . ID ,
Platform : "windows" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
UpgradeCode : "" ,
} ,
{
InstallScript : "install3.sh" ,
InstallerFile : tfr2 ,
StorageID : ins2 ,
Filename : "installer2.pkg" ,
Title : "Mac App" ,
Source : "apps" ,
Version : "1.0" ,
UserID : user1 . ID ,
Platform : "darwin" ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
BundleIdentifier : "com.example.macapp" ,
} ,
} )
require . NoError ( t , err )
softwareInstallers , err = ds . GetSoftwareInstallers ( ctx , team . ID )
require . NoError ( t , err )
require . Len ( t , softwareInstallers , 3 )
// Find the mac installer and verify upgrade_code is NULL
var macTitleID uint
for _ , si := range softwareInstallers {
if * si . TitleID != titleID && * si . TitleID != secondTitleID {
macTitleID = * si . TitleID
break
}
}
require . NotZero ( t , macTitleID )
macUpgradeCode := getUpgradeCodeForTitle ( macTitleID )
require . Nil ( t , macUpgradeCode )
// Clean up
err = ds . BatchSetSoftwareInstallers ( ctx , & team . ID , [ ] * fleet . UploadSoftwareInstallerPayload { } )
require . NoError ( t , err )
}
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
2026-03-17 19:04:33 +00:00
_ , err = ds . EnqueueSetupExperienceItems ( ctx , "darwin" , "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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
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 )
// 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 )
2026-02-09 20:24:37 +00:00
// Create a new team for .sh testing
teamSh , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team sh darwin test" } )
require . NoError ( t , err )
// Initially, darwin should not see any self-service installers in this team
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "darwin" , & teamSh . ID )
require . NoError ( t , err )
assert . False ( t , hasSelfService , "darwin should not see self-service before .sh is created" )
// Create a self-service .sh installer (stored as platform='linux', extension='sh')
// This should be visible to darwin hosts due to the .sh exception
_ , _ , err = ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
Title : "sh script for darwin" ,
Source : "sh_packages" ,
InstallScript : "#!/bin/bash\necho install" ,
TeamID : & teamSh . ID ,
Filename : "script.sh" ,
Platform : "linux" , // .sh files are stored as linux
Extension : "sh" ,
SelfService : true ,
UserID : user1 . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
} )
require . NoError ( t , err )
// Darwin host should now see self-service .sh package
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "darwin" , & teamSh . ID )
require . NoError ( t , err )
assert . True ( t , hasSelfService , "darwin host should see self-service .sh packages" )
// Linux host should also see it
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "linux" , & teamSh . ID )
require . NoError ( t , err )
assert . True ( t , hasSelfService , "linux host should see self-service .sh packages" )
// Windows host shouldn't see .sh packages
hasSelfService , err = ds . HasSelfServiceSoftwareInstallers ( ctx , "windows" , & teamSh . ID )
require . NoError ( t , err )
assert . False ( t , hasSelfService , "windows host should NOT see .sh packages" )
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 )
2026-03-13 20:47:09 +00:00
require . ErrorIs ( t , err , errDeleteInstallerWithAssociatedInstallPolicy )
2024-08-30 17:13:25 +00:00
_ , 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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2024-12-16 17:47:34 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2024-08-30 17:13:25 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2025-02-11 19:53:11 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2024-08-30 17:13:25 +00:00
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 ( ) )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
host3 := test . NewHost ( t , ds , "host3" , "" , "host3key" , "host3uuid" , time . Now ( ) )
2024-09-26 18:23:50 +00:00
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" } ,
}
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
software3 := [ ] fleet . Software {
{ Name : "Win Title 1" , Version : "11.0" , Source : "programs" , UpgradeCode : ptr . String ( "" ) } ,
{ Name : "Win Title 2" , Version : "11.0" , Source : "programs" , UpgradeCode : ptr . String ( "CODEEXISTS" ) } ,
{ Name : "Win Title 3" , Version : "11.0" , Source : "programs" , UpgradeCode : ptr . String ( "" ) } ,
{ Name : "Win Title 4" , Version : "11.0" , Source : "programs" , UpgradeCode : ptr . String ( "12345" ) } ,
{ Name : "Win Title 5" , Version : "11.0" , Source : "programs" , UpgradeCode : ptr . String ( "ABCDEF" ) } ,
}
2024-09-26 18:23:50 +00:00
_ , err := ds . UpdateHostSoftware ( ctx , host1 . ID , software1 )
require . NoError ( t , err )
_ , err = ds . UpdateHostSoftware ( ctx , host2 . ID , software2 )
require . NoError ( t , err )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
_ , err = ds . UpdateHostSoftware ( ctx , host3 . ID , software3 )
require . NoError ( t , err )
2024-09-26 18:23:50 +00:00
require . NoError ( t , ds . SyncHostsSoftware ( ctx , time . Now ( ) ) )
require . NoError ( t , ds . SyncHostsSoftwareTitles ( ctx , time . Now ( ) ) )
tests := [ ] struct {
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
name string
payload * fleet . UploadSoftwareInstallerPayload
expectedName string
expectedSource string
expectedUpgradeCode * string
2024-09-26 18:23:50 +00:00
} {
{
name : "title that already exists, no bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title" ,
Source : "apps" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "apps" ,
2024-09-26 18:23:50 +00:00
} ,
{
name : "title that already exists, mismatched bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title" ,
Source : "apps" ,
BundleIdentifier : "com.existing.bundle" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "apps" ,
2024-09-26 18:23:50 +00:00
} ,
{
name : "title that already exists but doesn't have a bundle identifier" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title Without Bundle" ,
Source : "apps" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "apps" ,
2024-09-26 18:23:50 +00:00
} ,
{
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" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "apps" ,
2024-09-26 18:23:50 +00:00
} ,
{
name : "title that doesn't exist, no bundle identifier in payload" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "New Title" ,
Source : "some_source" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "some_source" ,
2024-09-26 18:23:50 +00:00
} ,
{
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" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "some_source" ,
2024-09-26 18:23:50 +00:00
} ,
2026-02-18 17:52:06 +00:00
{
name : "title that already exists with bundle identifier" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title" ,
Source : "apps" ,
BundleIdentifier : "existing.title" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "apps" ,
2026-02-18 17:52:06 +00:00
} ,
{
name : "title that already exists with bundle identifier, different source" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Existing Title" ,
Source : "ios_apps" ,
BundleIdentifier : "existing.title" ,
} ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
expectedSource : "ios_apps" ,
} ,
{
name : "installer: no upgrade code, existing title: same name, no upgrade code" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Win Title 1" ,
Source : "programs" ,
} ,
expectedName : "Win Title 1" ,
expectedSource : "programs" ,
expectedUpgradeCode : ptr . String ( "" ) ,
} ,
{
name : "installer: no upgrade code, existing title: same name, has upgrade code" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Win Title 2" ,
Source : "programs" ,
} ,
expectedName : "Win Title 2" ,
expectedSource : "programs" ,
expectedUpgradeCode : ptr . String ( "CODEEXISTS" ) ,
} ,
{
name : "installer: has upgrade code, existing title: same name, no upgrade code" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Win Title 3" ,
Source : "programs" ,
UpgradeCode : "NEWCODE" ,
} ,
expectedName : "Win Title 3" ,
expectedSource : "programs" ,
expectedUpgradeCode : ptr . String ( "NEWCODE" ) ,
} ,
{
name : "installer: has upgrade code, existing title: same name, different upgrade code" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Win Title 4" ,
Source : "programs" ,
UpgradeCode : "DIFFERENTCODE" ,
} ,
expectedName : "Win Title 4" ,
expectedSource : "programs" ,
expectedUpgradeCode : ptr . String ( "DIFFERENTCODE" ) , // should make a new title
} ,
{
name : "installer: has upgrade code, existing title: same name, same upgrade code" ,
payload : & fleet . UploadSoftwareInstallerPayload {
Title : "Win Title 5" ,
Source : "programs" ,
UpgradeCode : "ABCDEF" ,
} ,
expectedName : "Win Title 5" ,
expectedSource : "programs" ,
expectedUpgradeCode : ptr . String ( "ABCDEF" ) ,
2026-02-18 17:52:06 +00:00
} ,
2024-09-26 18:23:50 +00:00
}
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 )
2026-02-18 17:52:06 +00:00
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
var actual struct {
Name string ` db:"name" `
Source string ` db:"source" `
UpgradeCode * string ` db:"upgrade_code" `
}
2026-02-18 17:52:06 +00:00
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
err := sqlx . GetContext ( ctx , q , & actual , ` SELECT name, source, upgrade_code FROM software_titles WHERE id = ? ` , id )
2026-02-18 17:52:06 +00:00
require . NoError ( t , err )
return nil
} )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
if tt . expectedName != "" {
require . Equal ( t , tt . expectedName , actual . Name )
}
require . Equal ( t , tt . expectedSource , actual . Source )
require . Equal ( t , tt . expectedUpgradeCode , actual . UpgradeCode )
2024-09-26 18:23:50 +00:00
} )
}
}
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 )
2026-03-13 20:47:09 +00:00
team1Policies , _ , err := ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 )
2026-03-13 20:47:09 +00:00
team1Policies , _ , err = ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 )
2026-03-13 20:47:09 +00:00
team1Policies , _ , err = ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2025-02-24 21:56:49 +00:00
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 )
2026-03-13 20:47:09 +00:00
noTeamPolicies , _ , err := ds . ListTeamPolicies ( ctx , fleet . PolicyNoTeamID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 )
2026-03-13 20:47:09 +00:00
team2Policies , _ , err := ds . ListTeamPolicies ( ctx , team2 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 )
2026-03-13 20:47:09 +00:00
team2Policies , _ , err = ds . ListTeamPolicies ( ctx , team2 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 )
2026-03-13 20:47:09 +00:00
team1Policies , _ , err = ds . ListTeamPolicies ( ctx , team1 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 )
2026-03-13 20:47:09 +00:00
team3Policies , _ , err := ds . ListTeamPolicies ( ctx , team3 . ID , fleet . ListOptions { } , fleet . ListOptions { } , "" )
2024-12-27 18:10:28 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
2025-02-11 19:53:11 +00:00
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 ) ,
2026-01-12 23:30:51 +00:00
} , nil )
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
}
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 )
2026-03-24 14:53:23 +00:00
// Simulate the scenario from issue #42260: an FMA version update creates
// a second row with the same storage_id but different version and is_active = 0.
// GetTeamsWithInstallerByHash must only return the active row.
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
_ , err := q . ExecContext ( ctx , `
INSERT INTO software_installers
( team_id , global_or_team_id , storage_id , filename , extension , version , platform , title_id ,
Override patch policy query (#42322)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41815
### Changes
- Extracted patch policy creation to `pkg/patch_policy`
- Added a `patch_query` column to the `software_installers` table
- By default that column is empty, and patch policies will generate with
the default query if so
- On app manifest ingestion, the appropriate entry in
`software_installers` will save the override "patch" query from the
manifest in patch_query
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Relied on integration test for FMA version pinning
## Database migrations
- [x] 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.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2026-03-25 14:32:41 +00:00
install_script_content_id , uninstall_script_content_id , is_active , url , package_ids , patch_query )
2026-03-24 14:53:23 +00:00
SELECT team_id , global_or_team_id , storage_id , filename , extension , ' old_version ' , platform , title_id ,
Override patch policy query (#42322)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41815
### Changes
- Extracted patch policy creation to `pkg/patch_policy`
- Added a `patch_query` column to the `software_installers` table
- By default that column is empty, and patch policies will generate with
the default query if so
- On app manifest ingestion, the appropriate entry in
`software_installers` will save the override "patch" query from the
manifest in patch_query
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Relied on integration test for FMA version pinning
## Database migrations
- [x] 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.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
2026-03-25 14:32:41 +00:00
install_script_content_id , uninstall_script_content_id , 0 , url , package_ids , patch_query
2026-03-24 14:53:23 +00:00
FROM software_installers WHERE id = ?
` , installer1NoTeam )
return err
} )
// Should still return only the active installer per team, not the inactive duplicate
installers , err = ds . GetTeamsWithInstallerByHash ( ctx , hash1 , "https://example.com/1" )
require . NoError ( t , err )
require . Len ( t , installers , 2 ) // No team + Team 1, each with 1 active installer
require . Len ( t , installers [ 0 ] , 1 )
require . Equal ( t , installer1NoTeam , installers [ 0 ] [ 0 ] . InstallerID )
require . Len ( t , installers [ 1 ] , 1 )
require . Equal ( t , installer1Team1 , installers [ 1 ] [ 0 ] . InstallerID )
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
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" ) )
2026-03-05 23:28:52 +00:00
require . ErrorContainsf ( t , err , ` "title-a" already exists with fleet "Team A". ` , "expected existsError for same-team duplicate title, got: %T: %v" , err , err )
2025-11-05 16:40:44 +00:00
}
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
func testAddSoftwareTitleToMatchingSoftware ( t * testing . T , ds * Datastore ) {
ctx := context . Background ( )
2026-03-13 18:32:07 +00:00
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
host1 := test . NewHost ( t , ds , "host1" , "" , "host1key" , "host1uuid" , time . Now ( ) )
software1 := [ ] fleet . Software {
2026-03-13 18:32:07 +00:00
{ Name : "Win Title" , Version : "1.0" , Source : "programs" , UpgradeCode : ptr . String ( "CODE_1" ) } ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
}
2026-03-13 18:32:07 +00:00
// create a vpp app
test . CreateInsertGlobalVPPToken ( t , ds )
app , err := ds . InsertVPPAppWithTeam ( ctx , & fleet . VPPApp {
VPPAppTeam : fleet . VPPAppTeam { VPPAppID : fleet . VPPAppID { AdamID : "adam_vpp_1" , Platform : "ios" } , DisplayName : ptr . String ( "VPP1" ) } ,
Name : "iOS Title" ,
BundleIdentifier : "com.foo" ,
} , nil )
require . NoError ( t , err )
host2 , err := ds . NewHost ( ctx , & fleet . Host {
Hostname : "ios-test" ,
OsqueryHostID : ptr . String ( "osquery-ios" ) ,
NodeKey : ptr . String ( "node-key-ios" ) ,
UUID : uuid . NewString ( ) ,
Platform : "ios" ,
} )
require . NoError ( t , err )
software2 := [ ] fleet . Software {
{ Name : "iOS Title" , Version : "1.0" , Source : "ios_apps" , BundleIdentifier : "com.foo" } ,
}
_ , err = ds . UpdateHostSoftware ( ctx , host1 . ID , software1 )
require . NoError ( t , err )
_ , err = ds . UpdateHostSoftware ( ctx , host2 . ID , software2 )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
require . NoError ( t , err )
require . NoError ( t , ds . SyncHostsSoftware ( ctx , time . Now ( ) ) )
require . NoError ( t , ds . SyncHostsSoftwareTitles ( ctx , time . Now ( ) ) )
// creates a second software title with the same name
payload := & fleet . UploadSoftwareInstallerPayload {
2026-03-13 18:32:07 +00:00
Title : "Win Title" ,
Source : "programs" ,
UpgradeCode : "CODE_2" ,
Filename : "something.msi" ,
Version : "1.0" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
}
2026-03-13 18:32:07 +00:00
_ , newTitleID , err := ds . MatchOrCreateSoftwareInstaller ( ctx , payload )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
require . NoError ( t , err )
2026-03-13 18:32:07 +00:00
require . NotEmpty ( t , newTitleID )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
ExecAdhocSQL ( t , ds , func ( q sqlx . ExtContext ) error {
var gotTitleID uint
err := sqlx . GetContext ( ctx , q , & gotTitleID , ` SELECT title_id FROM software WHERE name = ? ` , "Win Title" )
require . NoError ( t , err )
2026-03-13 18:32:07 +00:00
require . NotEqual ( t , newTitleID , gotTitleID ) // title with different upgrade code is new
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
return nil
} )
2026-03-13 18:32:07 +00:00
// check that host has the ios app installed and title is correct.
found , err := hostInstalledSoftware ( ds , ctx , host2 . ID )
require . NoError ( t , err )
require . Len ( t , found , 1 )
require . Equal ( t , app . TitleID , found [ 0 ] . ID )
// add macOS installer with the same bundle identifier
payloadMacOS := & fleet . UploadSoftwareInstallerPayload {
Title : "A Mac Title" ,
Source : "apps" ,
Platform : "darwin" ,
Filename : "something.pkg" ,
Version : "1.0" ,
BundleIdentifier : "com.foo" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
}
_ , titleIDMacOS , err := ds . MatchOrCreateSoftwareInstaller ( ctx , payloadMacOS )
require . NoError ( t , err )
require . NotEqual ( t , app . TitleID , titleIDMacOS )
// check that the installed ios app did not change title ID to the new installer
found , err = hostInstalledSoftware ( ds , ctx , host2 . ID )
require . NoError ( t , err )
require . Len ( t , found , 1 )
require . Equal ( t , app . TitleID , found [ 0 ] . ID )
Handle upgrade code in installer software title matching (#40129)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39858
~~Also implements the idea from this comment:
https://github.com/fleetdm/fleet/issues/37802#issuecomment-3715729822~~
Decided to move the FMA name overriding idea to another PR.
## Changes
- `getOrGenerateSoftwareInstallerTitleID` now attempts to find existing
titles by name or by upgrade code if possible. It will update the
upgrade code if possible.
- also updated `addSoftwareTitleToMatchingSoftware` to match _only_ by
upgrade code if the installer has an upgrade code
These are the assumptions (and tests mostly) I made:
| installer | existing title | result |
|------------------|-----------------------------------|----------------------------------------------------------------------------|
| no upgrade code | same name, no upgrade code | uses existing |
| no upgrade code | same name, has upgrade code | uses existing,
existing upgrade code stays |
| has upgrade code | same name, no upgrade code | uses existing,
existing title is updated with the incoming upgrade code |
| has upgrade code | same name, different upgrade code | new title is
created with same name |
| has upgrade code | same name, same upgrade code | uses existing |
| has upgrade code | different name, same upgrade code | uses existing,
~~existing title's name is updated~~ |
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] 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.
## Testing
- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
- Tested having the title `7-Zip 23.01 (x64)` with an upgrade code in
the db, added 7-Zip FMA, title became the existing `7-Zip 23.01 (x64)`
title with the same upgrade code.
- Trying to add it again fails with the correct error message
2026-02-20 22:09:41 +00:00
}
2026-02-26 20:27:16 +00:00
func testFleetMaintainedAppInstallerUpdates ( t * testing . T , ds * Datastore ) {
ctx := t . Context ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( "file contents" ) , t . TempDir )
require . NoError ( t , err )
maintainedApp , err := ds . UpsertMaintainedApp ( ctx , & fleet . MaintainedApp {
Name : "Maintained1" ,
Slug : "maintained1" ,
Platform : "darwin" ,
UniqueIdentifier : "fleet.maintained1" ,
} )
require . NoError ( t , err )
installerID , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
Title : "testpkg" ,
Source : "apps" ,
Platform : "darwin" ,
PreInstallQuery : "SELECT 1" ,
InstallScript : "echo install" ,
PostInstallScript : "echo post install" ,
UninstallScript : "echo uninstall" ,
InstallerFile : tfr ,
StorageID : "storageid1" ,
Filename : "test.pkg" ,
Version : "1.0" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
FleetMaintainedAppID : ptr . Uint ( maintainedApp . ID ) ,
InstallDuringSetup : ptr . Bool ( false ) ,
SelfService : false ,
} )
require . NoError ( t , err )
tmFilter := fleet . TeamFilter { User : test . UserAdmin }
titles , _ , _ , err := ds . ListSoftwareTitles ( ctx , fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( 0 ) , Platform : "darwin" , AvailableForInstall : true } , tmFilter )
require . NoError ( t , err )
require . Len ( t , titles , 1 )
require . False ( t , * titles [ 0 ] . SoftwarePackage . InstallDuringSetup )
require . False ( t , * titles [ 0 ] . SoftwarePackage . SelfService )
installer , err := ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
require . NoError ( t , err )
require . NotNil ( t , installer )
installScript := installer . InstallScriptContentID
postInstallScript := installer . PostInstallScriptContentID
uninstallScript := installer . UninstallScriptContentID
require . NotZero ( t , installScript )
require . NotZero ( t , postInstallScript )
require . NotZero ( t , uninstallScript )
require . Equal ( t , "SELECT 1" , installer . PreInstallQuery )
// batch add the installer with different scripts, setup experience, self service
err = ds . BatchSetSoftwareInstallers ( ctx , nil , [ ] * fleet . UploadSoftwareInstallerPayload {
{
Title : "testpkg" ,
Source : "apps" ,
PreInstallQuery : "SELECT 1 DIFFERENT" ,
InstallScript : "echo install 2" ,
PostInstallScript : "echo post install 2" ,
UninstallScript : "echo uninstall 2" ,
InstallerFile : tfr ,
StorageID : "storageid1" ,
Filename : "test.pkg" ,
Version : "1.0" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
FleetMaintainedAppID : ptr . Uint ( maintainedApp . ID ) ,
InstallDuringSetup : ptr . Bool ( true ) ,
SelfService : true ,
} ,
} )
require . NoError ( t , err )
titles , _ , _ , err = ds . ListSoftwareTitles ( ctx , fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( 0 ) , Platform : "darwin" , AvailableForInstall : true } , tmFilter )
require . NoError ( t , err )
require . Len ( t , titles , 1 )
require . True ( t , * titles [ 0 ] . SoftwarePackage . InstallDuringSetup )
require . True ( t , * titles [ 0 ] . SoftwarePackage . SelfService )
installer , err = ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
require . NoError ( t , err )
require . NotNil ( t , installer )
// all fields that should have changed did change
require . NotEqual ( t , installScript , installer . InstallScriptContentID )
require . NotEqual ( t , postInstallScript , installer . PostInstallScriptContentID )
require . NotEqual ( t , uninstallScript , installer . UninstallScriptContentID )
require . Equal ( t , "SELECT 1 DIFFERENT" , installer . PreInstallQuery )
}
Repoint policies before deleting installers (#41362)
When removing old installer rows, update policies.software_installer_id
to reference the new/active installer first to avoid FK constraint
failures (there is no ON DELETE CASCADE). For custom installers, repoint
policies that reference older versions before deleting them. For
fleet-maintained apps, collect keep IDs once, build the UPDATE via
sqlx.In to re-point policies that reference evicted versions to the
active installer, then delete the evicted rows. Adds error context for
query construction and execution failures.
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #
# Checklist for submitter
If some of the following don't apply, delete the relevant line.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects
## Testing
- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)
- [ ] QA'd all new/changed functionality manually
For unreleased bug fixes in a release candidate, one of:
- [x] Confirmed that the fix is not expected to adversely impact load
test results
---------
Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
2026-03-10 22:19:02 +00:00
func testRepointPolicyToNewInstaller ( t * testing . T , ds * Datastore ) {
ctx := t . Context ( )
user := test . NewUser ( t , ds , "Alice" , "alice@example.com" , true )
t . Run ( "custom_package" , func ( t * testing . T ) {
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team" + t . Name ( ) } )
require . NoError ( t , err )
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( "file contents" ) , t . TempDir )
require . NoError ( t , err )
installerID , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
Title : "testpkg" ,
Source : "apps" ,
Platform : "darwin" ,
PreInstallQuery : "SELECT 1" ,
InstallScript : "echo install" ,
PostInstallScript : "echo post install" ,
UninstallScript : "echo uninstall" ,
InstallerFile : tfr ,
StorageID : "storageid1" ,
Filename : "test.pkg" ,
Version : "1.0" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
InstallDuringSetup : ptr . Bool ( false ) ,
SelfService : false ,
TeamID : ptr . Uint ( team . ID ) ,
} )
require . NoError ( t , err )
policy , err := ds . NewTeamPolicy ( ctx , team . ID , & user . ID , fleet . PolicyPayload {
Name : "p1" ,
Query : "SELECT 1;" ,
SoftwareInstallerID : & installerID ,
} )
require . NoError ( t , err )
tmFilter := fleet . TeamFilter { User : test . UserAdmin , TeamID : ptr . Uint ( team . ID ) }
titles , _ , _ , err := ds . ListSoftwareTitles ( ctx , fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( team . ID ) , Platform : "darwin" , AvailableForInstall : true } , tmFilter )
require . NoError ( t , err )
require . Len ( t , titles , 1 )
require . False ( t , * titles [ 0 ] . SoftwarePackage . InstallDuringSetup )
require . False ( t , * titles [ 0 ] . SoftwarePackage . SelfService )
installer , err := ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
require . NoError ( t , err )
require . NotNil ( t , installer )
installScript := installer . InstallScriptContentID
postInstallScript := installer . PostInstallScriptContentID
uninstallScript := installer . UninstallScriptContentID
require . NotZero ( t , installScript )
require . NotZero ( t , postInstallScript )
require . NotZero ( t , uninstallScript )
require . Equal ( t , "SELECT 1" , installer . PreInstallQuery )
// batch add (gitops), this should succeed because we now update the pointer in the policy for the new version
err = ds . BatchSetSoftwareInstallers ( ctx , ptr . Uint ( team . ID ) , [ ] * fleet . UploadSoftwareInstallerPayload {
{
Title : "testpkg" ,
Source : "apps" ,
Platform : "darwin" ,
PreInstallQuery : "SELECT 1 DIFFERENT" ,
InstallScript : "echo install 2" ,
PostInstallScript : "echo post install 2" ,
UninstallScript : "echo uninstall 2" ,
InstallerFile : tfr ,
StorageID : "storageid1" ,
Filename : "test.pkg" ,
Version : "2.0" , // Note the new version, this means we evict version 1.0 because it's a custom package
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
InstallDuringSetup : ptr . Bool ( true ) ,
SelfService : true ,
TeamID : ptr . Uint ( team . ID ) ,
} ,
} )
require . NoError ( t , err )
titles , _ , _ , err = ds . ListSoftwareTitles ( ctx , fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( team . ID ) , Platform : "darwin" , AvailableForInstall : true } , tmFilter )
require . NoError ( t , err )
require . Len ( t , titles , 1 )
require . Len ( t , titles [ 0 ] . SoftwarePackage . AutomaticInstallPolicies , 1 )
require . Equal ( t , policy . ID , titles [ 0 ] . SoftwarePackage . AutomaticInstallPolicies [ 0 ] . ID )
metadata , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , ptr . Uint ( team . ID ) , titles [ 0 ] . ID , false )
require . NoError ( t , err )
policyAfterUpdate , err := ds . TeamPolicy ( ctx , team . ID , policy . ID )
require . NoError ( t , err )
require . Equal ( t , metadata . InstallerID , * policyAfterUpdate . SoftwareInstallerID )
} )
t . Run ( "fma" , func ( t * testing . T ) {
team , err := ds . NewTeam ( ctx , & fleet . Team { Name : "team" + t . Name ( ) } )
require . NoError ( t , err )
fma , err := ds . UpsertMaintainedApp ( ctx , & fleet . MaintainedApp { ID : 1 } )
require . NoError ( t , err )
tfr , err := fleet . NewTempFileReader ( strings . NewReader ( "file contents" ) , t . TempDir )
require . NoError ( t , err )
installerID , _ , err := ds . MatchOrCreateSoftwareInstaller ( ctx , & fleet . UploadSoftwareInstallerPayload {
FleetMaintainedAppID : ptr . Uint ( fma . ID ) ,
Title : "testpkg_fma" ,
Source : "apps" ,
Platform : "darwin" ,
PreInstallQuery : "SELECT 1" ,
InstallScript : "echo install" ,
PostInstallScript : "echo post install" ,
UninstallScript : "echo uninstall" ,
InstallerFile : tfr ,
StorageID : "storageid1" ,
Filename : "test_fma.pkg" ,
Version : "1.0" ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
InstallDuringSetup : ptr . Bool ( false ) ,
SelfService : false ,
TeamID : ptr . Uint ( team . ID ) ,
} )
require . NoError ( t , err )
policy , err := ds . NewTeamPolicy ( ctx , team . ID , & user . ID , fleet . PolicyPayload {
Name : "p2" ,
Query : "SELECT 1;" ,
SoftwareInstallerID : & installerID ,
} )
require . NoError ( t , err )
tmFilter := fleet . TeamFilter { User : test . UserAdmin , TeamID : ptr . Uint ( team . ID ) }
titles , _ , _ , err := ds . ListSoftwareTitles ( ctx , fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( team . ID ) , Platform : "darwin" , AvailableForInstall : true } , tmFilter )
require . NoError ( t , err )
require . Len ( t , titles , 1 )
require . False ( t , * titles [ 0 ] . SoftwarePackage . InstallDuringSetup )
require . False ( t , * titles [ 0 ] . SoftwarePackage . SelfService )
installer , err := ds . GetSoftwareInstallerMetadataByID ( ctx , installerID )
require . NoError ( t , err )
require . NotNil ( t , installer )
installScript := installer . InstallScriptContentID
postInstallScript := installer . PostInstallScriptContentID
uninstallScript := installer . UninstallScriptContentID
require . NotZero ( t , installScript )
require . NotZero ( t , postInstallScript )
require . NotZero ( t , uninstallScript )
require . Equal ( t , "SELECT 1" , installer . PreInstallQuery )
for i := 2 ; i <= 3 ; i ++ {
// Simulate multiple gitops runs that each increment the FMA version.
// This will lead to v1.0 getting evicted.
err = ds . BatchSetSoftwareInstallers ( ctx , ptr . Uint ( team . ID ) , [ ] * fleet . UploadSoftwareInstallerPayload {
{
FleetMaintainedAppID : ptr . Uint ( fma . ID ) ,
Title : "testpkg_fma" ,
Source : "apps" ,
Platform : "darwin" ,
PreInstallQuery : "SELECT 1 DIFFERENT" ,
InstallScript : "echo install 2" ,
PostInstallScript : "echo post install 2" ,
UninstallScript : "echo uninstall 2" ,
InstallerFile : tfr ,
StorageID : "storageid2" ,
Filename : "test_fma.pkg" ,
Version : fmt . Sprintf ( "%d.0" , i ) ,
UserID : user . ID ,
ValidatedLabels : & fleet . LabelIdentsWithScope { } ,
InstallDuringSetup : ptr . Bool ( true ) ,
SelfService : true ,
TeamID : ptr . Uint ( team . ID ) ,
} ,
} )
require . NoError ( t , err )
}
titles , _ , _ , err = ds . ListSoftwareTitles ( ctx , fleet . SoftwareTitleListOptions { TeamID : ptr . Uint ( team . ID ) , Platform : "darwin" , AvailableForInstall : true } , tmFilter )
require . NoError ( t , err )
require . Len ( t , titles , 1 )
require . Len ( t , titles [ 0 ] . SoftwarePackage . AutomaticInstallPolicies , 1 )
require . Equal ( t , policy . ID , titles [ 0 ] . SoftwarePackage . AutomaticInstallPolicies [ 0 ] . ID )
metadata , err := ds . GetSoftwareInstallerMetadataByTeamAndTitleID ( ctx , ptr . Uint ( team . ID ) , titles [ 0 ] . ID , false )
require . NoError ( t , err )
policyAfterUpdate , err := ds . TeamPolicy ( ctx , team . ID , policy . ID )
require . NoError ( t , err )
require . Equal ( t , metadata . InstallerID , * policyAfterUpdate . SoftwareInstallerID )
} )
}