package mysql import ( "bytes" "context" "database/sql" "errors" "fmt" "os" "path/filepath" "sort" "strings" "testing" "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxdb" "github.com/fleetdm/fleet/v4/server/datastore/filesystem" "github.com/fleetdm/fleet/v4/server/fleet" common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql" "github.com/fleetdm/fleet/v4/server/platform/mysql/testing_utils" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/fleetdm/fleet/v4/server/test" "github.com/google/uuid" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestSoftwareInstallers(t *testing.T) { ds := CreateMySQLDS(t) cases := []struct { name string fn func(t *testing.T, ds *Datastore) }{ {"SoftwareInstallRequests", testSoftwareInstallRequests}, {"ListPendingSoftwareInstalls", testListPendingSoftwareInstalls}, {"GetSoftwareInstallResults", testGetSoftwareInstallResult}, {"CleanupUnusedSoftwareInstallers", testCleanupUnusedSoftwareInstallers}, {"BatchSetSoftwareInstallers", testBatchSetSoftwareInstallers}, {"BatchSetSoftwareInstallersWithUpgradeCodes", testBatchSetSoftwareInstallersWithUpgradeCodes}, {"GetSoftwareInstallerMetadataByTeamAndTitleID", testGetSoftwareInstallerMetadataByTeamAndTitleID}, {"HasSelfServiceSoftwareInstallers", testHasSelfServiceSoftwareInstallers}, {"DeleteSoftwareInstallers", testDeleteSoftwareInstallers}, {"testDeletePendingSoftwareInstallsForPolicy", testDeletePendingSoftwareInstallsForPolicy}, {"GetHostLastInstallData", testGetHostLastInstallData}, {"GetOrGenerateSoftwareInstallerTitleID", testGetOrGenerateSoftwareInstallerTitleID}, {"BatchSetSoftwareInstallersScopedViaLabels", testBatchSetSoftwareInstallersScopedViaLabels}, {"MatchOrCreateSoftwareInstallerWithAutomaticPolicies", testMatchOrCreateSoftwareInstallerWithAutomaticPolicies}, {"GetDetailsForUninstallFromExecutionID", testGetDetailsForUninstallFromExecutionID}, {"GetTeamsWithInstallerByHash", testGetTeamsWithInstallerByHash}, {"MatchOrCreateSoftwareInstallerDuplicateHash", testMatchOrCreateSoftwareInstallerDuplicateHash}, {"BatchSetSoftwareInstallersSetupExperienceSideEffects", testBatchSetSoftwareInstallersSetupExperienceSideEffects}, {"EditDeleteSoftwareInstallersActivateNextActivity", testEditDeleteSoftwareInstallersActivateNextActivity}, {"BatchSetSoftwareInstallersActivateNextActivity", testBatchSetSoftwareInstallersActivateNextActivity}, {"SoftwareInstallerReplicaLag", testSoftwareInstallerReplicaLag}, {"SoftwareTitleDisplayName", testSoftwareTitleDisplayName}, {"AddSoftwareTitleToMatchingSoftware", testAddSoftwareTitleToMatchingSoftware}, {"FleetMaintainedAppInstallerUpdates", testFleetMaintainedAppInstallerUpdates}, {"RepointCustomPackagePolicyToNewInstaller", testRepointPolicyToNewInstaller}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { defer TruncateTables(t, ds) c.fn(t, ds) }) } } func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) { ctx := context.Background() t.Cleanup(func() { ds.testActivateSpecificNextActivities = nil }) 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()) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) err := ds.UpsertSecretVariables(ctx, []fleet.SecretVariable{ { Name: "RUBBER", Value: "DUCKY", }, { Name: "BIG", Value: "BIRD", }, { Name: "COOKIE", Value: "MONSTER", }, }) require.NoError(t, err) tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) require.NoError(t, err) installerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "hello $FLEET_SECRET_RUBBER", PreInstallQuery: "SELECT 1", PostInstallScript: "world $FLEET_SECRET_BIG", UninstallScript: "goodbye $FLEET_SECRET_COOKIE", InstallerFile: tfr1, StorageID: "storage1", Filename: "file1", Title: "file1", Version: "1.0", Source: "apps", UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) tfr2, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) require.NoError(t, err) installerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "world", PreInstallQuery: "SELECT 2", PostInstallScript: "hello", InstallerFile: tfr2, StorageID: "storage2", Filename: "file2", Title: "file2", Version: "2.0", Source: "apps", UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) tfr3, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) require.NoError(t, err) installerID3, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "banana", PreInstallQuery: "SELECT 3", PostInstallScript: "apple", InstallerFile: tfr3, StorageID: "storage3", Filename: "file3", Title: "file3", Version: "3.0", Source: "apps", SelfService: true, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) // 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{}) require.NoError(t, err) time.Sleep(time.Millisecond) hostInstall3, err := ds.InsertSoftwareInstallRequest(ctx, host2.ID, installerID1, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) time.Sleep(time.Millisecond) hostInstall4, err := ds.InsertSoftwareInstallRequest(ctx, host2.ID, installerID2, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) 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, "") require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host2.ID, InstallUUID: hostInstall4, InstallScriptExitCode: ptr.Int(0), }, nil) require.NoError(t, err) // 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, "") require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host2.ID, InstallUUID: hostInstall5, PreInstallConditionOutput: ptr.String(""), // pre-install query did not return results, so install failed }, nil) 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)) require.Contains(t, installDetailsList1, hostInstall1) require.Contains(t, installDetailsList1, hostInstall2) require.Contains(t, installDetailsList2, hostInstall3) exec1, err := ds.GetSoftwareInstallDetails(ctx, hostInstall1) require.NoError(t, err) require.Equal(t, host1.ID, exec1.HostID) require.Equal(t, hostInstall1, exec1.ExecutionID) require.Equal(t, "hello DUCKY", exec1.InstallScript) require.Equal(t, "world BIRD", exec1.PostInstallScript) require.Equal(t, installerID1, exec1.InstallerID) require.Equal(t, "SELECT 1", exec1.PreInstallCondition) require.False(t, exec1.SelfService) assert.Equal(t, "goodbye MONSTER", exec1.UninstallScript) // Check that regular install has MaxRetries = 0 require.EqualValues(t, 0, exec1.MaxRetries, "Regular install should have MaxRetries = 0") // 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, "") require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host1.ID, InstallUUID: hostInstall6, PreInstallConditionOutput: ptr.String("output"), }, nil) 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) // Create install request, don't fulfil it, delete and restore host. // Should not appear in list of pending installs for that host. _, err = ds.InsertSoftwareInstallRequest(ctx, host3.ID, installerID1, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) // Set LastEnrolledAt before deleting the host (simulating a DEP enrolled host) host3.LastEnrolledAt = time.Now() 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) // 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, ` INSERT INTO setup_experience_status_results (host_uuid, name, status, software_installer_id, host_software_installs_execution_id) 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) } func testSoftwareInstallRequests(t *testing.T, ds *Datastore) { ctx := context.Background() // create a team team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"}) require.NoError(t, err) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) createBuiltinLabels(t, ds) labelsByName, err := ds.LabelIDsByName(ctx, []string{fleet.BuiltinLabelNameAllHosts}, fleet.TeamFilter{}) require.NoError(t, err) require.Len(t, labelsByName, 1) cases := map[string]*uint{ "no team": nil, "team": &team.ID, } for tc, teamID := range cases { t.Run(tc, func(t *testing.T) { // non-existent installer si, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, teamID, 1, false) var nfe fleet.NotFoundError require.ErrorAs(t, err, &nfe) require.Nil(t, si) installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ Title: "foo", Source: "bar", InstallScript: "echo", TeamID: teamID, Filename: "foo.pkg", UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) installerMeta, err := ds.GetSoftwareInstallerMetadataByID(ctx, installerID) require.NoError(t, err) require.NotNil(t, installerMeta.TitleID) require.Equal(t, titleID, *installerMeta.TitleID) si, err = ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, teamID, *installerMeta.TitleID, false) require.NoError(t, err) require.NotNil(t, si) require.Equal(t, "foo.pkg", si.Name) 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) // non-existent host _, err = ds.InsertSoftwareInstallRequest(ctx, 12, si.InstallerID, fleet.HostSoftwareInstallOptions{}) require.ErrorAs(t, err, &nfe) // 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) _, err = ds.InsertSoftwareInstallRequest(ctx, hostPendingInstall.ID, si.InstallerID, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) // 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) // 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) execID, err := ds.InsertSoftwareInstallRequest(ctx, hostFailedInstall.ID, si.InstallerID, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: hostFailedInstall.ID, InstallUUID: execID, InstallScriptExitCode: ptr.Int(1), }, nil) require.NoError(t, err) // 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', '')`, 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) // 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), UUID: uuid.NewString(), Platform: "darwin", TeamID: teamID, }) require.NoError(t, err) execID, err = ds.InsertSoftwareInstallRequest(ctx, hostInstalled.ID, si.InstallerID, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: hostInstalled.ID, InstallUUID: execID, InstallScriptExitCode: ptr.Int(0), }, nil) require.NoError(t, err) // 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', '')`, 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) // 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) err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostPendingUninstall.ID, si.InstallerID, false) 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) execID = "uuid" + tag + tc err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostFailedUninstall.ID, si.InstallerID, false) require.NoError(t, err) _, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{ HostID: hostFailedUninstall.ID, ExecutionID: execID, ExitCode: 1, }, nil) require.NoError(t, err) // 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) execID = "uuid" + tag + tc err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostUninstalled.ID, si.InstallerID, false) require.NoError(t, err) _, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{ HostID: hostUninstalled.ID, ExecutionID: execID, ExitCode: 0, }, nil) require.NoError(t, err) // Uninstall request with unknown host err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, 99999, si.InstallerID, false) assert.ErrorContains(t, err, "Host") 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) } userTeamFilter := fleet.TeamFilter{ User: &fleet.User{GlobalRole: ptr.String("admin")}, } // 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 { hostNames := make([]string, 0, len(hosts)) for _, h := range hosts { hostNames = append(hostNames, h.Hostname) } return hostNames } 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)) }) } summary, err := ds.GetSummaryHostSoftwareInstalls(ctx, installerMeta.InstallerID) require.NoError(t, err) require.Equal(t, fleet.SoftwareInstallerStatusSummary{ Installed: 1, PendingInstall: 1, FailedInstall: 1, PendingUninstall: 1, FailedUninstall: 1, }, *summary) vppSummary, err := ds.GetSummaryHostInHouseAppInstalls(ctx, teamID, inHouseID) require.NoError(t, err) require.Equal(t, fleet.VPPAppStatusSummary{ Installed: 1, Pending: 1, Failed: 1, }, *vppSummary) }) } } 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 user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) for _, tc := range []struct { name string expectedStatus fleet.SoftwareInstallerStatus postInstallScriptEC *int preInstallQueryOutput *string installScriptEC *int postInstallScriptOutput *string installScriptOutput *string }{ { name: "pending install", expectedStatus: fleet.SoftwareInstallPending, postInstallScriptOutput: ptr.String("post install output"), installScriptOutput: ptr.String("install output"), }, { name: "failing install post install script", expectedStatus: fleet.SoftwareInstallFailed, postInstallScriptEC: ptr.Int(1), postInstallScriptOutput: ptr.String("post install output"), installScriptOutput: ptr.String("install output"), }, { name: "failing install install script", expectedStatus: fleet.SoftwareInstallFailed, installScriptEC: ptr.Int(1), postInstallScriptOutput: ptr.String("post install output"), installScriptOutput: ptr.String("install output"), }, { name: "failing install pre install query", expectedStatus: fleet.SoftwareInstallFailed, 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" installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ 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{}, }) 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) beforeInstallRequest := time.Now() installUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, fleet.HostSoftwareInstallOptions{}) require.NoError(t, err) 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() _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host.ID, InstallUUID: installUUID, PreInstallConditionOutput: tc.preInstallQueryOutput, InstallScriptExitCode: tc.installScriptEC, InstallScriptOutput: tc.installScriptOutput, PostInstallScriptExitCode: tc.postInstallScriptEC, PostInstallScriptOutput: tc.postInstallScriptOutput, }, nil) require.NoError(t, err) // 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) // delete installer to confirm that we can still access the install record (unless pending) err = ds.DeleteSoftwareInstaller(ctx, installerID) require.NoError(t, err) if tc.expectedStatus == fleet.SoftwareInstallPending { // expect pending to be deleted _, err = ds.GetSoftwareInstallResults(ctx, installUUID) require.Error(t, err, notFound("HostSoftwareInstallerResult")) return } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { // ensure version is not changed, though we don't expose it yet 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 }) res, err = ds.GetSoftwareInstallResults(ctx, installUUID) require.NoError(t, err) require.Equal(t, installUUID, res.InstallUUID) require.Equal(t, tc.expectedStatus, res.Status) require.Equal(t, swFilename, res.SoftwarePackage) require.Equal(t, host.ID, res.HostID) require.Equal(t, tc.preInstallQueryOutput, res.PreInstallQueryOutput) require.Equal(t, tc.postInstallScriptOutput, res.PostInstallScriptOutput) require.Equal(t, tc.installScriptOutput, res.Output) require.NotNil(t, res.CreatedAt) require.Equal(t, createdAt, res.CreatedAt) require.NotNil(t, res.UpdatedAt) require.Less(t, beforeInstallResult, *res.UpdatedAt) }) } } func testCleanupUnusedSoftwareInstallers(t *testing.T, ds *Datastore) { 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) 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 err = ds.CleanupUnusedSoftwareInstallers(ctx, store, time.Now()) 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) _, _ = ins0File.Seek(0, 0) tfr0, err := fleet.NewTempFileReader(ins0File, t.TempDir) require.NoError(t, err) assertExisting([]string{ins0}) swi, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "install", InstallerFile: tfr0, StorageID: ins0, Filename: "installer0", Title: "ins0", Source: "apps", UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) assertExisting([]string{ins0}) err = ds.CleanupUnusedSoftwareInstallers(ctx, store, time.Now()) require.NoError(t, err) assertExisting([]string{ins0}) // remove it from the DB, will now cleanup err = ds.DeleteSoftwareInstaller(ctx, swi) require.NoError(t, err) // 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)) require.NoError(t, err) assertExisting(nil) } func testBatchSetSoftwareInstallers(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 couple hosts host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now()) host2 := test.NewHost(t, ds, "host2", "2", "host2key", "host2uuid", time.Now()) err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID, host2.ID})) require.NoError(t, err) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) // TODO(roberto): perform better assertions, we should have everything // 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 { meta, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, title.ID, false) require.NoError(t, err) require.NotNil(t, meta.TitleID) } } // batch set with everything empty err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, nil) require.NoError(t, err) softwareInstallers, err := ds.GetSoftwareInstallers(ctx, team.ID) require.NoError(t, err) require.Empty(t, softwareInstallers) assertSoftware(nil) err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{}) require.NoError(t, err) softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID) require.NoError(t, err) require.Empty(t, softwareInstallers) assertSoftware(nil) // add a single installer ins0 := "installer0" ins0File := bytes.NewReader([]byte("installer0")) tfr0, err := fleet.NewTempFileReader(ins0File, t.TempDir) require.NoError(t, err) displayName := "Display name 1" maintainedApp, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ Name: "Maintained1", Slug: "maintained1", Platform: "darwin", 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", DisplayName: displayName, FleetMaintainedAppID: ptr.Uint(maintainedApp.ID), }}) 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].TeamID) require.Equal(t, team.ID, *softwareInstallers[0].TeamID) require.NotNil(t, softwareInstallers[0].TitleID) require.Equal(t, "https://example.com", softwareInstallers[0].URL) require.Equal(t, maintainedApp.ID, *softwareInstallers[0].FleetMaintainedAppID) assertSoftware([]fleet.SoftwareTitle{ {Name: ins0, Source: "apps", ExtensionFor: ""}, }) meta, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, *softwareInstallers[0].TitleID, false) require.NoError(t, err) require.Equal(t, displayName, meta.DisplayName) // add a new installer + ins0 installer // mark ins0 as install_during_setup 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", 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{ {Name: ins0, Source: "apps", ExtensionFor: ""}, {Name: ins1, Source: "apps", ExtensionFor: ""}, }) // remove ins0 fails due to install_during_setup 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.Error(t, err) require.ErrorIs(t, err, errDeleteInstallerInstalledDuringSetup) // 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", 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: nil, 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", ValidatedLabels: &fleet.LabelIdentsWithScope{}, }, }) require.NoError(t, err) // mark ins0 as NOT install_during_setup 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", DisplayName: displayName, InstallDuringSetup: ptr.Bool(false), 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", DisplayName: displayName, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }, }) require.NoError(t, err) softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID) require.NoError(t, err) require.Len(t, softwareInstallers, 2) ins0TitleID := softwareInstallers[0].TitleID // remove ins0 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, DisplayName: displayName, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }, }) 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) require.NotNil(t, softwareInstallers[0].TeamID) require.Empty(t, softwareInstallers[0].URL) assertSoftware([]fleet.SoftwareTitle{ {Name: ins1, Source: "apps", ExtensionFor: ""}, }) // display name is deleted for ins0 _, err = ds.getSoftwareTitleDisplayName(ctx, team.ID, *ins0TitleID) require.ErrorContains(t, err, "not found") 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) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host2.ID, InstallUUID: execID2, InstallScriptExitCode: ptr.Int(0), }, nil) 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) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host2.ID, InstallUUID: execID2b, InstallScriptExitCode: ptr.Int(1), }, nil) 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) // 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{ {Name: ins0, Source: "apps", ExtensionFor: "", BundleIdentifier: ptr.String("com.example.different.ins0")}, }) // 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{ {Name: "ins0-different", Source: "apps", ExtensionFor: "", BundleIdentifier: ptr.String("com.example.ins0")}, }) // remove everything err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{}) require.NoError(t, err) softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID) require.NoError(t, err) require.Empty(t, softwareInstallers) assertSoftware([]fleet.SoftwareTitle{}) // 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) } 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) } 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()) err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID})) 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{ {Name: ins0, Source: "apps", ExtensionFor: ""}, {Name: ins1, Source: "apps", ExtensionFor: ""}, }) // Add setup_experience_status_results for both installers _, err = ds.EnqueueSetupExperienceItems(ctx, "darwin", "darwin", host1.UUID, *host1.TeamID) 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), }, nil) 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) } } } func testGetSoftwareInstallerMetadataByTeamAndTitleID(t *testing.T, ds *Datastore) { ctx := context.Background() team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"}) require.NoError(t, err) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) installerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ Title: "foo", Source: "bar", InstallScript: "echo install", PostInstallScript: "echo post-install", PreInstallQuery: "SELECT 1", TeamID: &team.ID, Filename: "foo.pkg", Platform: "darwin", UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) installerMeta, err := ds.GetSoftwareInstallerMetadataByID(ctx, installerID) require.NoError(t, err) require.Equal(t, "darwin", installerMeta.Platform) metaByTeamAndTitle, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, *installerMeta.TitleID, true) 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) installerID, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ Title: "bar", Source: "bar", InstallScript: "echo install", TeamID: &team.ID, Filename: "foo.pkg", UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) installerMeta, err = ds.GetSoftwareInstallerMetadataByID(ctx, installerID) require.NoError(t, err) metaByTeamAndTitle, err = ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, *installerMeta.TitleID, true) 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) } func testHasSelfServiceSoftwareInstallers(t *testing.T, ds *Datastore) { ctx := context.Background() team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"}) require.NoError(t, err) user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) test.CreateInsertGlobalVPPToken(t, ds) 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 _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ Title: "foo", Source: "bar", InstallScript: "echo install", TeamID: &team.ID, Filename: "foo.pkg", Platform: platform, SelfService: false, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) 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 _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ Title: "foo2", Source: "bar2", InstallScript: "echo install", TeamID: &team.ID, Filename: "foo2.pkg", Platform: platform, SelfService: true, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) 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 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) // Create a global self-service installer _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ Title: "foo global", Source: "bar", InstallScript: "echo install", TeamID: nil, Filename: "foo global.pkg", Platform: platform, SelfService: true, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) 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) // 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") // 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 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) assert.True(t, hasSelfService) } func testDeleteSoftwareInstallers(t *testing.T, ds *Datastore) { 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) _, _ = ins0File.Seek(0, 0) tfr0, err := fleet.NewTempFileReader(ins0File, t.TempDir) require.NoError(t, err) team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"}) require.NoError(t, err) softwareInstallerID, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "install", InstallerFile: tfr0, StorageID: ins0, Filename: "installer.pkg", Title: "ins0", Source: "apps", Platform: "darwin", TeamID: &team1.ID, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) p1, err := ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{ Name: "p1", Query: "SELECT 1;", SoftwareInstallerID: &softwareInstallerID, }) require.NoError(t, err) err = ds.DeleteSoftwareInstaller(ctx, softwareInstallerID) require.Error(t, err) require.ErrorIs(t, err, errDeleteInstallerWithAssociatedInstallPolicy) _, err = ds.DeleteTeamPolicies(ctx, team1.ID, []uint{p1.ID}) require.NoError(t, err) // 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 }) err = ds.DeleteSoftwareInstaller(ctx, softwareInstallerID) require.NoError(t, err) // deleting again returns an error, no such installer err = ds.DeleteSoftwareInstaller(ctx, softwareInstallerID) var nfe *common_mysql.NotFoundError require.ErrorAs(t, err, &nfe) } 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{ InstallScript: "install", InstallerFile: tfr0, StorageID: ins0, Filename: "installer.pkg", Title: "ins0", Source: "apps", Platform: "darwin", TeamID: &team1.ID, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) 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{ InstallScript: "install", InstallerFile: tfr0, StorageID: ins0, Filename: "installer.pkg", Title: "ins1", Source: "apps", Platform: "darwin", TeamID: &team1.ID, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) 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 executionID, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID1, fleet.HostSoftwareInstallOptions{PolicyID: &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) 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 executionID, err = ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID2, fleet.HostSoftwareInstallOptions{PolicyID: &policy2.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) 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 executionID, err = ds.InsertSoftwareInstallRequest(ctx, host2.ID, installerID1, fleet.HostSoftwareInstallOptions{PolicyID: &policy1.ID}) require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host2.ID, InstallUUID: executionID, InstallScriptExitCode: ptr.Int(0), }, nil) 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) } 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) _, _ = ins0File.Seek(0, 0) tfr0, err := fleet.NewTempFileReader(ins0File, t.TempDir) require.NoError(t, err) softwareInstallerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "install", InstallerFile: tfr0, StorageID: ins0, Filename: "installer.pkg", Title: "ins1", Source: "apps", Platform: "darwin", TeamID: &team1.ID, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) softwareInstallerID2, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallScript: "install2", InstallerFile: tfr0, StorageID: ins0, Filename: "installer2.pkg", Title: "ins2", Source: "apps", Platform: "darwin", TeamID: &team1.ID, UserID: user1.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) 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. installUUID1, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID1, fleet.HostSoftwareInstallOptions{}) 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) require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status) // Set result of last installation. _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host1.ID, InstallUUID: installUUID1, InstallScriptExitCode: ptr.Int(0), }, nil) 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) require.Equal(t, fleet.SoftwareInstalled, *host1LastInstall.Status) // Install installer2.pkg on host1. installUUID2, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID2, fleet.HostSoftwareInstallOptions{}) 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) require.Equal(t, fleet.SoftwareInstalled, *host1LastInstall.Status) // 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) require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status) // Perform another installation of installer1.pkg. installUUID3, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID1, fleet.HostSoftwareInstallOptions{}) 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) require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status) // 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. _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host1.ID, InstallUUID: installUUID2, InstallScriptExitCode: ptr.Int(0), }, nil) require.NoError(t, err) _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host1.ID, InstallUUID: installUUID3, InstallScriptExitCode: ptr.Int(1), }, nil) 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) require.Equal(t, fleet.SoftwareInstallFailed, *host1LastInstall.Status) // 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) } 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()) host3 := test.NewHost(t, ds, "host3", "", "host3key", "host3uuid", time.Now()) software1 := []fleet.Software{ {Name: "Existing Title", Version: "0.0.1", Source: "apps", BundleIdentifier: "existing.title"}, } software2 := []fleet.Software{ {Name: "Existing Title", Version: "v0.0.2", Source: "apps", BundleIdentifier: "existing.title"}, {Name: "Existing Title", Version: "0.0.3", Source: "apps", BundleIdentifier: "existing.title"}, {Name: "Existing Title Without Bundle", Version: "0.0.3", Source: "apps"}, } 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")}, } _, err := ds.UpdateHostSoftware(ctx, host1.ID, software1) require.NoError(t, err) _, err = ds.UpdateHostSoftware(ctx, host2.ID, software2) require.NoError(t, err) _, err = ds.UpdateHostSoftware(ctx, host3.ID, software3) require.NoError(t, err) require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now())) require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now())) tests := []struct { name string payload *fleet.UploadSoftwareInstallerPayload expectedName string expectedSource string expectedUpgradeCode *string }{ { name: "title that already exists, no bundle identifier in payload", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "Existing Title", Source: "apps", }, expectedSource: "apps", }, { name: "title that already exists, mismatched bundle identifier in payload", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "Existing Title", Source: "apps", BundleIdentifier: "com.existing.bundle", }, expectedSource: "apps", }, { name: "title that already exists but doesn't have a bundle identifier", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "Existing Title Without Bundle", Source: "apps", }, expectedSource: "apps", }, { name: "title that already exists, no bundle identifier in DB, bundle identifier in payload", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "Existing Title Without Bundle", Source: "apps", BundleIdentifier: "com.new.bundleid", }, expectedSource: "apps", }, { name: "title that doesn't exist, no bundle identifier in payload", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "New Title", Source: "some_source", }, expectedSource: "some_source", }, { name: "title that doesn't exist, with bundle identifier in payload", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "New Title With Bundle", Source: "some_source", BundleIdentifier: "com.new.bundle", }, expectedSource: "some_source", }, { name: "title that already exists with bundle identifier", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "Existing Title", Source: "apps", BundleIdentifier: "existing.title", }, expectedSource: "apps", }, { name: "title that already exists with bundle identifier, different source", payload: &fleet.UploadSoftwareInstallerPayload{ Title: "Existing Title", Source: "ios_apps", BundleIdentifier: "existing.title", }, 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"), }, } 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) var actual struct { Name string `db:"name"` Source string `db:"source"` UpgradeCode *string `db:"upgrade_code"` } ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { err := sqlx.GetContext(ctx, q, &actual, `SELECT name, source, upgrade_code FROM software_titles WHERE id = ?`, id) require.NoError(t, err) return nil }) if tt.expectedName != "" { require.Equal(t, tt.expectedName, actual.Name) } require.Equal(t, tt.expectedSource, actual.Source) require.Equal(t, tt.expectedUpgradeCode, actual.UpgradeCode) }) } } 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. err := ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(teamID, []uint{host.ID})) 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 = ? AND title_id IN (SELECT id FROM software_titles WHERE name = ? AND source = ? AND extension_for = '')`, globalOrTeamID, payload.Installer.Title, payload.Installer.Source) return err }) _, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, swID, fleet.HostSoftwareInstallOptions{}) 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") } } } } } func testMatchOrCreateSoftwareInstallerWithAutomaticPolicies(t *testing.T, ds *Datastore) { ctx := context.Background() user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true) team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 1"}) require.NoError(t, err) team2, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 2"}) require.NoError(t, err) tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir) require.NoError(t, err) // Test pkg without automatic install doesn't create policy. _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallerFile: tfr1, BundleIdentifier: "com.manual.foobar", Extension: "pkg", StorageID: "storage0", Filename: "foobar0", Title: "Manual foobar", Version: "1.0", Source: "apps", UserID: user1.ID, TeamID: &team1.ID, AutomaticInstall: false, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) team1Policies, _, err := ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Empty(t, team1Policies) // Test pkg. installerID1, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallerFile: tfr1, BundleIdentifier: "com.foo.bar", Extension: "pkg", StorageID: "storage1", Filename: "foobar1", Title: "Foobar", Version: "1.0", Source: "apps", UserID: user1.ID, TeamID: &team1.ID, AutomaticInstall: true, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, team1Policies, 1) require.Equal(t, "[Install software] Foobar (pkg)", team1Policies[0].Name) require.Equal(t, "SELECT 1 FROM apps WHERE bundle_identifier = 'com.foo.bar';", team1Policies[0].Query) require.Equal(t, "Policy triggers automatic install of Foobar on each host that's missing this software.", team1Policies[0].Description) require.Equal(t, "darwin", team1Policies[0].Platform) require.NotNil(t, team1Policies[0].SoftwareInstallerID) require.Equal(t, installerID1, *team1Policies[0].SoftwareInstallerID) require.NotNil(t, team1Policies[0].TeamID) require.Equal(t, team1.ID, *team1Policies[0].TeamID) // Test Mac FMA fma, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ID: 1}) require.NoError(t, err) installerFMA, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ 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{}, }) require.NoError(t, err) team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, team1Policies, 2) require.Equal(t, "[Install software] FooFMA", team1Policies[1].Name) require.Equal(t, "SELECT 1 FROM osquery_info", team1Policies[1].Query) 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) // 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) // 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) noTeamPolicies, _, err := ds.ListTeamPolicies(ctx, fleet.PolicyNoTeamID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, noTeamPolicies, 1) require.Equal(t, "[Install software] Zoobar (msi)", noTeamPolicies[0].Name) require.Equal(t, "SELECT 1 FROM programs WHERE identifying_number = 'id1';", noTeamPolicies[0].Query) require.Equal(t, "Policy triggers automatic install of Zoobar on each host that's missing this software.", noTeamPolicies[0].Description) require.Equal(t, "windows", noTeamPolicies[0].Platform) require.NotNil(t, noTeamPolicies[0].SoftwareInstallerID) require.Equal(t, installerID2, *noTeamPolicies[0].SoftwareInstallerID) require.NotNil(t, noTeamPolicies[0].TeamID) require.Equal(t, fleet.PolicyNoTeamID, *noTeamPolicies[0].TeamID) // Test deb. installerID3, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallerFile: tfr1, Extension: "deb", StorageID: "storage3", Filename: "barfoo1", Title: "Barfoo", Version: "1.0", Source: "deb_packages", UserID: user1.ID, TeamID: &team2.ID, AutomaticInstall: true, PackageIDs: []string{"id1"}, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) team2Policies, _, err := ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, team2Policies, 1) require.Equal(t, "[Install software] Barfoo (deb)", team2Policies[0].Name) require.Equal(t, `SELECT 1 WHERE EXISTS ( SELECT 1 WHERE (SELECT COUNT(*) FROM deb_packages) = 0 ) OR EXISTS ( SELECT 1 FROM deb_packages WHERE name = 'Barfoo' AND status = 'install ok installed' );`, team2Policies[0].Query) require.Equal(t, `Policy triggers automatic install of Barfoo on each host that's missing this software. Software won't be installed on Linux hosts with RPM-based distributions because this policy's query is written to always pass on these hosts.`, team2Policies[0].Description) require.Equal(t, "linux", team2Policies[0].Platform) require.NotNil(t, team2Policies[0].SoftwareInstallerID) require.Equal(t, installerID3, *team2Policies[0].SoftwareInstallerID) require.NotNil(t, team2Policies[0].TeamID) require.Equal(t, team2.ID, *team2Policies[0].TeamID) // Test rpm. installerID4, _, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallerFile: tfr1, Extension: "rpm", StorageID: "storage4", Filename: "barzoo1", Title: "Barzoo", Version: "1.0", Source: "rpm_packages", UserID: user1.ID, TeamID: &team2.ID, AutomaticInstall: true, PackageIDs: []string{"id1"}, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) team2Policies, _, err = ds.ListTeamPolicies(ctx, team2.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, team2Policies, 2) require.Equal(t, "[Install software] Barzoo (rpm)", team2Policies[1].Name) require.Equal(t, `SELECT 1 WHERE EXISTS ( SELECT 1 WHERE (SELECT COUNT(*) FROM rpm_packages) = 0 ) OR EXISTS ( SELECT 1 FROM rpm_packages WHERE name = 'Barzoo' );`, team2Policies[1].Query) require.Equal(t, `Policy triggers automatic install of Barzoo on each host that's missing this software. Software won't be installed on Linux hosts with Debian-based distributions because this policy's query is written to always pass on these hosts.`, team2Policies[1].Description) require.Equal(t, "linux", team2Policies[1].Platform) require.NotNil(t, team2Policies[0].SoftwareInstallerID) require.Equal(t, installerID4, *team2Policies[1].SoftwareInstallerID) require.NotNil(t, team2Policies[1].TeamID) require.Equal(t, team2.ID, *team2Policies[1].TeamID) _, err = ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{ Name: "[Install software] OtherFoobar (pkg)", Query: "SELECT 1;", }) require.NoError(t, err) // Test pkg and policy with name already exists. _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallerFile: tfr1, BundleIdentifier: "com.foo2.bar2", Extension: "pkg", StorageID: "storage5", Filename: "foobar5", Title: "OtherFoobar", Version: "2.0", Source: "apps", UserID: user1.ID, TeamID: &team1.ID, AutomaticInstall: true, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) team1Policies, _, err = ds.ListTeamPolicies(ctx, team1.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, team1Policies, 4) require.Equal(t, "[Install software] OtherFoobar (pkg) 2", team1Policies[3].Name) team3, err := ds.NewTeam(ctx, &fleet.Team{Name: "team 3"}) require.NoError(t, err) _, err = ds.NewTeamPolicy(ctx, team3.ID, &user1.ID, fleet.PolicyPayload{ Name: "[Install software] Something2 (msi)", Query: "SELECT 1;", }) require.NoError(t, err) _, err = ds.NewTeamPolicy(ctx, team3.ID, &user1.ID, fleet.PolicyPayload{ Name: "[Install software] Something2 (msi) 2", Query: "SELECT 1;", }) require.NoError(t, err) // This name is on another team, so it shouldn't count. _, err = ds.NewTeamPolicy(ctx, team1.ID, &user1.ID, fleet.PolicyPayload{ Name: "[Install software] Something2 (msi) 3", Query: "SELECT 1;", }) require.NoError(t, err) // Test msi and policy with name already exists. _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{ InstallerFile: tfr1, Extension: "msi", StorageID: "storage6", Filename: "foobar6", Title: "Something2", PackageIDs: []string{"id2"}, Version: "2.0", Source: "programs", UserID: user1.ID, TeamID: &team3.ID, AutomaticInstall: true, ValidatedLabels: &fleet.LabelIdentsWithScope{}, }) require.NoError(t, err) team3Policies, _, err := ds.ListTeamPolicies(ctx, team3.ID, fleet.ListOptions{}, fleet.ListOptions{}, "") require.NoError(t, err) require.Len(t, team3Policies, 3) require.Equal(t, "[Install software] Something2 (msi) 3", team3Policies[2].Name) } func testGetDetailsForUninstallFromExecutionID(t *testing.T, ds *Datastore) { 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 title, selfService, err := ds.GetDetailsForUninstallFromExecutionID(ctx, "unknown") require.ErrorIs(t, err, sql.ErrNoRows) require.Empty(t, title) require.False(t, selfService) // 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) _, _, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req1) require.ErrorIs(t, err, sql.ErrNoRows) // record a result for req1, will be deleted from upcoming_activities _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host.ID, InstallUUID: req1, InstallScriptExitCode: ptr.Int(0), }, nil) require.NoError(t, err) _, _, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req1) require.ErrorIs(t, err, sql.ErrNoRows) // create an uninstall request for installer1 req3 := uuid.NewString() err = ds.InsertSoftwareUninstallRequest(ctx, req3, host.ID, installer1, true) require.NoError(t, err) title, selfService, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req3) require.NoError(t, err) require.Equal(t, "foobar", title) require.True(t, selfService) // record a result for req2, will activate req3 so it is now in host_software_installs too _, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{ HostID: host.ID, InstallUUID: req2, InstallScriptExitCode: ptr.Int(0), }, nil) require.NoError(t, err) title, selfService, err = ds.GetDetailsForUninstallFromExecutionID(ctx, req3) require.NoError(t, err) require.Equal(t, "foobar", title) require.True(t, selfService) } 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) // 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) // 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) require.Len(t, installers[0], 1) require.Equal(t, installer1NoTeam, installers[0][0].InstallerID) require.Nil(t, installers[0][0].TeamID) 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) for _, is := range installers { i := is[0] 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) 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) // 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, install_script_content_id, uninstall_script_content_id, is_active, url, package_ids, patch_query) SELECT team_id, global_or_team_id, storage_id, filename, extension, 'old_version', platform, title_id, install_script_content_id, uninstall_script_content_id, 0, url, package_ids, patch_query 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) } 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) } 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) } 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()) installerID, 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) // 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{ DisplayName: ptr.String("update1"), 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{ 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) // 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), DisplayName: ptr.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.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) // 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") // 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") } 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") // Binary packages with same title on same team → reject _, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, mkPayload(&teamA.ID, "a.sh", "title-a")) require.ErrorContainsf(t, err, `"title-a" already exists with fleet "Team A".`, "expected existsError for same-team duplicate title, got: %T: %v", err, err) } func testAddSoftwareTitleToMatchingSoftware(t *testing.T, ds *Datastore) { ctx := context.Background() user := test.NewUser(t, ds, "Alice", "alice@example.com", true) host1 := test.NewHost(t, ds, "host1", "", "host1key", "host1uuid", time.Now()) software1 := []fleet.Software{ {Name: "Win Title", Version: "1.0", Source: "programs", UpgradeCode: ptr.String("CODE_1")}, } // 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) 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{ Title: "Win Title", Source: "programs", UpgradeCode: "CODE_2", Filename: "something.msi", Version: "1.0", UserID: user.ID, ValidatedLabels: &fleet.LabelIdentsWithScope{}, } _, newTitleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, payload) require.NoError(t, err) require.NotEmpty(t, newTitleID) 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) require.NotEqual(t, newTitleID, gotTitleID) // title with different upgrade code is new return nil }) // 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) } 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) } 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) }) }