fleet/server/datastore/mysql/software_installers_test.go

3874 lines
142 KiB
Go
Raw Normal View History

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/datastore/mysql/common_mysql"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql/testing_utils"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
2024-09-07 13:07:22 +00:00
"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},
{"GetSoftwareInstallerMetadataByTeamAndTitleID", testGetSoftwareInstallerMetadataByTeamAndTitleID},
{"HasSelfServiceSoftwareInstallers", testHasSelfServiceSoftwareInstallers},
{"DeleteSoftwareInstallers", testDeleteSoftwareInstallers},
{"testDeletePendingSoftwareInstallsForPolicy", testDeletePendingSoftwareInstallsForPolicy},
{"GetHostLastInstallData", testGetHostLastInstallData},
{"GetOrGenerateSoftwareInstallerTitleID", testGetOrGenerateSoftwareInstallerTitleID},
{"BatchSetSoftwareInstallersScopedViaLabels", testBatchSetSoftwareInstallersScopedViaLabels},
{"MatchOrCreateSoftwareInstallerWithAutomaticPolicies", testMatchOrCreateSoftwareInstallerWithAutomaticPolicies},
{"GetDetailsForUninstallFromExecutionID", testGetDetailsForUninstallFromExecutionID},
{"GetTeamsWithInstallerByHash", testGetTeamsWithInstallerByHash},
{"MatchOrCreateSoftwareInstallerDuplicateHash", testMatchOrCreateSoftwareInstallerDuplicateHash},
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355) Still adding tests but wanted to get this up for review of the overall "shape" of the fix When certain things happen like installer updates we delete pending upcoming_activities(UA) and host_software_install(HSI) entries and need to mark setup_experience_status_results(SESR) cancelled. When this happens if that UA/HSI are being depended on by setup experience we need to make sure that that setup experience result eventually gets marked failed. I kind of went back and forth a few times on how best to do this and avoid race conditions. One thing I tried was looking at existence of the UA/HSI but found that naively just trying to look at that in relation to the SESR entry seemed to have a few race conditions that were hard to resolve. There are a few possible states here we need to account for such as: un-activated, totally not yet running software install cancelled activated but not yet running on the host software install cancelled activated and running on the host software install cancelled before results are completely reported back What I eventually came around to was essentially that we want to mark the SESR cancelled in the same transaction we delete the HSI/UA in. We then finalize it by marking it failed and sending the activity the next time the host fetches setupm experience results. The new cancelled status never leaves fleet. This is a bit ugly but in my testing avoided the race conditions and works well. Note that to actually avoid setup experience hanging entirely we still need to fix #29357 which encompasses several cases where the unified queue can get completely stuck for a host # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes - [ ] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [ ] Added/updated automated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Make sure fleetd is compatible with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)). - [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). - [ ] For unreleased bug fixes in a release candidate, confirmed that the fix is not expected to adversely impact load test results or alerted the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
{"BatchSetSoftwareInstallersSetupExperienceSideEffects", testBatchSetSoftwareInstallersSetupExperienceSideEffects},
{"EditDeleteSoftwareInstallersActivateNextActivity", testEditDeleteSoftwareInstallersActivateNextActivity},
{"BatchSetSoftwareInstallersActivateNextActivity", testBatchSetSoftwareInstallersActivateNextActivity},
{"SaveInstallerUpdatesClearsFleetMaintainedAppID", testSaveInstallerUpdatesClearsFleetMaintainedAppID},
{"SoftwareInstallerReplicaLag", testSoftwareInstallerReplicaLag},
{"SoftwareTitleDisplayName", testSoftwareTitleDisplayName},
}
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{
2024-05-29 15:01:48 +00:00
InstallScript: "banana",
PreInstallQuery: "SELECT 3",
PostInstallScript: "apple",
InstallerFile: tfr3,
2024-05-29 15:01:48 +00:00
StorageID: "storage3",
Filename: "file3",
Title: "file3",
Version: "3.0",
Source: "apps",
SelfService: true,
UserID: user1.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
2024-05-29 15:01:48 +00:00
})
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),
})
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,
2024-09-07 13:07:22 +00:00
PreInstallConditionOutput: ptr.String(""), // pre-install query did not return results, so install failed
})
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)
2024-05-29 15:01:48 +00:00
require.False(t, exec1.SelfService)
assert.Equal(t, "goodbye MONSTER", exec1.UninstallScript)
Added support for retry logic in setup experience software installations. (#32823) Fixes #32580 - Added retry logic for software installs - Added sending intermediate results to Fleet server I QA'd this on Linux (see video below). For macOS and Windows, I QA'd it by having the server force retries on normal (non-setup experience software installs). Demo video: https://youtu.be/dbu78G6bXf8 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] Verified that fleetd runs on macOS, Linux and Windows - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Automatic retries for software installations, with exponential backoff on transient/network errors. - Intermediate failures are recorded without closing the original request; subsequent attempts continue automatically. - Activity feed entries are created for intermediate failures, including install identifiers. - Setup experience installs now retry automatically (up to 3 attempts). - Tests - Expanded test coverage for retry behavior, error classification, and intermediate failure reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 17:26:14 +00:00
// Check that regular install has MaxRetries = 0
require.EqualValues(t, 0, exec1.MaxRetries, "Regular install should have MaxRetries = 0")
2024-05-29 15:01:48 +00:00
// add a self-service request for installerID3 on host1
hostInstall6, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID3, fleet.HostSoftwareInstallOptions{SelfService: true})
require.NoError(t, err)
ds.testActivateSpecificNextActivities = []string{hostInstall6}
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host1.ID, "")
2024-05-29 15:01:48 +00:00
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
2024-05-29 15:01:48 +00:00
HostID: host1.ID,
InstallUUID: hostInstall6,
PreInstallConditionOutput: ptr.String("output"),
})
require.NoError(t, err)
exec2, err := ds.GetSoftwareInstallDetails(ctx, hostInstall6)
require.NoError(t, err)
require.Equal(t, host1.ID, exec2.HostID)
require.Equal(t, hostInstall6, exec2.ExecutionID)
require.Equal(t, "banana", exec2.InstallScript)
require.Equal(t, "apple", exec2.PostInstallScript)
require.Equal(t, installerID3, exec2.InstallerID)
require.Equal(t, "SELECT 3", exec2.PreInstallCondition)
require.True(t, exec2.SelfService)
// 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)
Updated SQL modes in tests to match production. (#31445) Fixes #31444 The changes are primarily in tests. The only changes in production code are a couple validations/checks for invalid values in: - mysql/apple_mdm.go - mysql/hosts.go - mysql/queries.go # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved handling of timestamp and default values across various features to prevent database errors and warnings. * Enhanced validation and data consistency for Apple Business Manager tokens and MDM profiles. * Updated test data and logic to comply with stricter database constraints and realistic scenarios, including date handling and field lengths. * **Chores** * Updated test setups to reflect schema changes, improve data integrity, and avoid future compatibility issues. * Standardized SQL mode and timestamp usage in test environments. * Refined test data for VPP apps, software installers, and device enrollments for better reliability. * **Tests** * Expanded and updated tests to cover new fields, stricter validation, and more accurate simulation of real-world conditions. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-08-03 06:18:13 +00:00
// 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)
Added support for retry logic in setup experience software installations. (#32823) Fixes #32580 - Added retry logic for software installs - Added sending intermediate results to Fleet server I QA'd this on Linux (see video below). For macOS and Windows, I QA'd it by having the server force retries on normal (non-setup experience software installs). Demo video: https://youtu.be/dbu78G6bXf8 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] Verified that fleetd runs on macOS, Linux and Windows - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Automatic retries for software installations, with exponential backoff on transient/network errors. - Intermediate failures are recorded without closing the original request; subsequent attempts continue automatically. - Activity feed entries are created for intermediate failures, including install identifiers. - Setup experience installs now retry automatically (up to 3 attempts). - Tests - Expanded test coverage for retry behavior, error classification, and intermediate failure reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 17:26:14 +00:00
// Test MaxRetries for setup experience install
// Create a software install request that's part of setup experience
setupExperienceInstallID, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, installerID1, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
// Insert a setup experience status result to simulate this install is part of setup experience
_, err = ds.writer(ctx).ExecContext(ctx, `
INSERT INTO setup_experience_status_results
(host_uuid, name, status, software_installer_id, host_software_installs_execution_id)
Added support for retry logic in setup experience software installations. (#32823) Fixes #32580 - Added retry logic for software installs - Added sending intermediate results to Fleet server I QA'd this on Linux (see video below). For macOS and Windows, I QA'd it by having the server force retries on normal (non-setup experience software installs). Demo video: https://youtu.be/dbu78G6bXf8 # Checklist for submitter - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] Verified that fleetd runs on macOS, Linux and Windows - [x] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Automatic retries for software installations, with exponential backoff on transient/network errors. - Intermediate failures are recorded without closing the original request; subsequent attempts continue automatically. - Activity feed entries are created for intermediate failures, including install identifiers. - Setup experience installs now retry automatically (up to 3 attempts). - Tests - Expanded test coverage for retry behavior, error classification, and intermediate failure reporting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-16 17:26:14 +00:00
VALUES (?, ?, ?, ?, ?)`,
host1.UUID, "test_software", fleet.SetupExperienceStatusPending, installerID1, setupExperienceInstallID)
require.NoError(t, err)
// Get the install details and check MaxRetries = setupExperienceSoftwareInstallsRetries
setupExperienceInstallDetails, err := ds.GetSoftwareInstallDetails(ctx, setupExperienceInstallID)
require.NoError(t, err)
require.Equal(t, host1.ID, setupExperienceInstallDetails.HostID)
require.Equal(t, setupExperienceInstallID, setupExperienceInstallDetails.ExecutionID)
require.Equal(t, setupExperienceSoftwareInstallsRetries, setupExperienceInstallDetails.MaxRetries, "Setup experience install should have MaxRetries = %d", setupExperienceSoftwareInstallsRetries)
}
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)
2024-09-07 13:07:22 +00:00
// Host with software install pending
tag := "-pending_install"
hostPendingInstall, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
NodeKey: ptr.String("node-key-macos" + tag + tc),
UUID: uuid.NewString(),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
_, err = ds.InsertSoftwareInstallRequest(ctx, hostPendingInstall.ID, si.InstallerID, fleet.HostSoftwareInstallOptions{})
2024-09-07 13:07:22 +00:00
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)
2024-09-07 13:07:22 +00:00
// Host with software install failed
tag = "-failed_install"
hostFailedInstall, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
NodeKey: ptr.String("node-key-macos" + tag + tc),
UUID: uuid.NewString(),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
execID, err := ds.InsertSoftwareInstallRequest(ctx, hostFailedInstall.ID, si.InstallerID, fleet.HostSoftwareInstallOptions{})
2024-09-07 13:07:22 +00:00
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: hostFailedInstall.ID,
InstallUUID: execID,
InstallScriptExitCode: ptr.Int(1),
2024-09-07 13:07:22 +00:00
})
require.NoError(t, err)
2024-09-07 13:07:22 +00:00
// Host with in-house app failed install
tag = "-in-house-failed_install"
hostInHouseFailedInstall, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "ios-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-ios" + tag + tc),
NodeKey: ptr.String("node-key-ios" + tag + tc),
UUID: uuid.NewString(),
Platform: "ios",
TeamID: teamID,
})
require.NoError(t, err)
nanoEnroll(t, ds, hostInHouseFailedInstall, false)
cmdUUID := uuid.NewString()
err = ds.InsertHostInHouseAppInstall(ctx, hostInHouseFailedInstall.ID, inHouseID, inHouseTitleID, cmdUUID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
// record a failed verification for that in-house app install
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO nano_command_results (id, command_uuid, status, result)
VALUES (?, ?, 'Error', '<?xml version="1.0"?><plist></plist>')`,
hostInHouseFailedInstall.UUID, cmdUUID)
if err != nil {
return err
}
_, err = q.ExecContext(ctx, `
UPDATE host_in_house_software_installs
SET verification_command_uuid = ?, verification_failed_at = NOW(6)
WHERE command_uuid = ? AND host_id = ?`,
uuid.NewString(), cmdUUID, hostInHouseFailedInstall.ID,
)
return err
})
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), hostInHouseFailedInstall.ID, cmdUUID)
require.NoError(t, err)
2024-09-07 13:07:22 +00:00
// Host with software install successful
tag = "-installed"
hostInstalled, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
NodeKey: ptr.String("node-key-macos" + tag + tc),
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),
2024-09-07 13:07:22 +00:00
})
require.NoError(t, err)
2024-09-07 13:07:22 +00:00
// host with in-house successful install
tag = "-in-house-installed"
hostInHouseInstalled, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "ios-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-ios" + tag + tc),
NodeKey: ptr.String("node-key-ios" + tag + tc),
UUID: uuid.NewString(),
Platform: "ios",
TeamID: teamID,
})
require.NoError(t, err)
nanoEnroll(t, ds, hostInHouseInstalled, false)
cmdUUID = uuid.NewString()
err = ds.InsertHostInHouseAppInstall(ctx, hostInHouseInstalled.ID, inHouseID, inHouseTitleID, cmdUUID, fleet.HostSoftwareInstallOptions{})
require.NoError(t, err)
// record a successful verification for that in-house app install
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `
INSERT INTO nano_command_results (id, command_uuid, status, result)
VALUES (?, ?, 'Acknowledged', '<?xml version="1.0"?><plist></plist>')`,
hostInHouseInstalled.UUID, cmdUUID)
if err != nil {
return err
}
_, err = q.ExecContext(ctx, `
UPDATE host_in_house_software_installs
SET verification_command_uuid = ?, verification_at = NOW(6)
WHERE command_uuid = ? AND host_id = ?`,
uuid.NewString(), cmdUUID, hostInHouseInstalled.ID,
)
return err
})
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), hostInHouseInstalled.ID, cmdUUID)
require.NoError(t, err)
2024-09-07 13:07:22 +00:00
// Host with pending uninstall
tag = "-pending_uninstall"
hostPendingUninstall, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
NodeKey: ptr.String("node-key-macos" + tag + tc),
UUID: uuid.NewString(),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostPendingUninstall.ID, si.InstallerID, false)
2024-09-07 13:07:22 +00:00
require.NoError(t, err)
// Host with failed uninstall
tag = "-failed_uninstall"
hostFailedUninstall, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
NodeKey: ptr.String("node-key-macos" + tag + tc),
UUID: uuid.NewString(),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
execID = "uuid" + tag + tc
err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostFailedUninstall.ID, si.InstallerID, false)
2024-09-07 13:07:22 +00:00
require.NoError(t, err)
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: hostFailedUninstall.ID,
ExecutionID: execID,
ExitCode: 1,
2024-09-07 13:07:22 +00:00
})
require.NoError(t, err)
2024-09-07 13:07:22 +00:00
// Host with successful uninstall
tag = "-uninstalled"
hostUninstalled, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-test" + tag + tc,
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
NodeKey: ptr.String("node-key-macos" + tag + tc),
UUID: uuid.NewString(),
Platform: "darwin",
TeamID: teamID,
})
require.NoError(t, err)
execID = "uuid" + tag + tc
err = ds.InsertSoftwareUninstallRequest(ctx, execID, hostUninstalled.ID, si.InstallerID, false)
2024-09-07 13:07:22 +00:00
require.NoError(t, err)
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
HostID: hostUninstalled.ID,
ExecutionID: execID,
ExitCode: 0,
2024-09-07 13:07:22 +00:00
})
require.NoError(t, err)
2024-09-09 19:43:52 +00:00
// Uninstall request with unknown host
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, 99999, si.InstallerID, false)
2024-09-09 19:43:52 +00:00
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")},
}
2024-09-07 13:07:22 +00:00
// for this test, teamID is nil for no-team, but the ListHosts filter
// returns "all teams" if TeamFilter = nil, it needs to use TeamFilter =
// 0 for "no team" only.
teamFilter := teamID
if teamFilter == nil {
teamFilter = ptr.Uint(0)
}
// get the names of hosts, useful for debugging
getHostNames := func(hosts []*fleet.Host) []string {
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{
2024-09-09 19:43:52 +00:00
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",
2024-09-04 21:46:48 +00:00
expectedStatus: fleet.SoftwareInstallPending,
postInstallScriptOutput: ptr.String("post install output"),
installScriptOutput: ptr.String("install output"),
},
{
name: "failing install post install script",
2024-09-04 21:46:48 +00:00
expectedStatus: fleet.SoftwareInstallFailed,
postInstallScriptEC: ptr.Int(1),
postInstallScriptOutput: ptr.String("post install output"),
installScriptOutput: ptr.String("install output"),
},
{
name: "failing install install script",
2024-09-04 21:46:48 +00:00
expectedStatus: fleet.SoftwareInstallFailed,
installScriptEC: ptr.Int(1),
postInstallScriptOutput: ptr.String("post install output"),
installScriptOutput: ptr.String("install output"),
},
{
name: "failing install pre install query",
2024-09-04 21:46:48 +00:00
expectedStatus: fleet.SoftwareInstallFailed,
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,
})
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),
})
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),
})
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)
}
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355) Still adding tests but wanted to get this up for review of the overall "shape" of the fix When certain things happen like installer updates we delete pending upcoming_activities(UA) and host_software_install(HSI) entries and need to mark setup_experience_status_results(SESR) cancelled. When this happens if that UA/HSI are being depended on by setup experience we need to make sure that that setup experience result eventually gets marked failed. I kind of went back and forth a few times on how best to do this and avoid race conditions. One thing I tried was looking at existence of the UA/HSI but found that naively just trying to look at that in relation to the SESR entry seemed to have a few race conditions that were hard to resolve. There are a few possible states here we need to account for such as: un-activated, totally not yet running software install cancelled activated but not yet running on the host software install cancelled activated and running on the host software install cancelled before results are completely reported back What I eventually came around to was essentially that we want to mark the SESR cancelled in the same transaction we delete the HSI/UA in. We then finalize it by marking it failed and sending the activity the next time the host fetches setupm experience results. The new cancelled status never leaves fleet. This is a bit ugly but in my testing avoided the race conditions and works well. Note that to actually avoid setup experience hanging entirely we still need to fix #29357 which encompasses several cases where the unified queue can get completely stuck for a host # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes - [ ] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [ ] Added/updated automated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Make sure fleetd is compatible with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)). - [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). - [ ] For unreleased bug fixes in a release candidate, confirmed that the fix is not expected to adversely impact load test results or alerted the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
func testBatchSetSoftwareInstallersSetupExperienceSideEffects(t *testing.T, ds *Datastore) {
ctx := context.Background()
t.Cleanup(func() { ds.testActivateSpecificNextActivities = nil })
// create a team
team, err := ds.NewTeam(ctx, &fleet.Team{Name: t.Name()})
require.NoError(t, err)
// create a host
host1 := test.NewHost(t, ds, "host1", "1", "host1key", "host1uuid", time.Now())
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID}))
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355) Still adding tests but wanted to get this up for review of the overall "shape" of the fix When certain things happen like installer updates we delete pending upcoming_activities(UA) and host_software_install(HSI) entries and need to mark setup_experience_status_results(SESR) cancelled. When this happens if that UA/HSI are being depended on by setup experience we need to make sure that that setup experience result eventually gets marked failed. I kind of went back and forth a few times on how best to do this and avoid race conditions. One thing I tried was looking at existence of the UA/HSI but found that naively just trying to look at that in relation to the SESR entry seemed to have a few race conditions that were hard to resolve. There are a few possible states here we need to account for such as: un-activated, totally not yet running software install cancelled activated but not yet running on the host software install cancelled activated and running on the host software install cancelled before results are completely reported back What I eventually came around to was essentially that we want to mark the SESR cancelled in the same transaction we delete the HSI/UA in. We then finalize it by marking it failed and sending the activity the next time the host fetches setupm experience results. The new cancelled status never leaves fleet. This is a bit ugly but in my testing avoided the race conditions and works well. Note that to actually avoid setup experience hanging entirely we still need to fix #29357 which encompasses several cases where the unified queue can get completely stuck for a host # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes - [ ] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [ ] Added/updated automated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Make sure fleetd is compatible with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)). - [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). - [ ] For unreleased bug fixes in a release candidate, confirmed that the fix is not expected to adversely impact load test results or alerted the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
host1.TeamID = &team.ID
require.NoError(t, err)
user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
assertSoftware := func(wantTitles []fleet.SoftwareTitle) {
tmFilter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
titles, _, _, err := ds.ListSoftwareTitles(
ctx,
fleet.SoftwareTitleListOptions{TeamID: &team.ID},
tmFilter,
)
require.NoError(t, err)
require.Len(t, titles, len(wantTitles))
for _, title := range titles {
meta, err := ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, &team.ID, title.ID, false)
require.NoError(t, err)
require.NotNil(t, meta.TitleID)
}
}
// add two installers
ins0 := "installer0"
ins0File := bytes.NewReader([]byte("installer0"))
tfr0, err := fleet.NewTempFileReader(ins0File, t.TempDir)
require.NoError(t, err)
ins1 := "installer1"
ins1File := bytes.NewReader([]byte("installer1"))
tfr1, err := fleet.NewTempFileReader(ins1File, t.TempDir)
require.NoError(t, err)
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install",
InstallerFile: tfr0,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
{
InstallScript: "install",
PostInstallScript: "post-install",
InstallerFile: tfr1,
StorageID: ins1,
Filename: ins1,
Title: ins1,
Source: "apps",
Version: "2",
PreInstallQuery: "select 1 from bar;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example2.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
})
require.NoError(t, err)
softwareInstallers, err := ds.GetSoftwareInstallers(ctx, team.ID)
require.NoError(t, err)
require.Len(t, softwareInstallers, 2)
require.NotNil(t, softwareInstallers[0].TitleID)
require.NotNil(t, softwareInstallers[0].TeamID)
require.Equal(t, team.ID, *softwareInstallers[0].TeamID)
require.Equal(t, "https://example.com", softwareInstallers[0].URL)
require.NotNil(t, softwareInstallers[1].TitleID)
require.NotNil(t, softwareInstallers[1].TeamID)
require.Equal(t, team.ID, *softwareInstallers[1].TeamID)
require.Equal(t, "https://example2.com", softwareInstallers[1].URL)
assertSoftware([]fleet.SoftwareTitle{
{Name: ins0, Source: "apps", ExtensionFor: ""},
{Name: ins1, Source: "apps", ExtensionFor: ""},
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355) Still adding tests but wanted to get this up for review of the overall "shape" of the fix When certain things happen like installer updates we delete pending upcoming_activities(UA) and host_software_install(HSI) entries and need to mark setup_experience_status_results(SESR) cancelled. When this happens if that UA/HSI are being depended on by setup experience we need to make sure that that setup experience result eventually gets marked failed. I kind of went back and forth a few times on how best to do this and avoid race conditions. One thing I tried was looking at existence of the UA/HSI but found that naively just trying to look at that in relation to the SESR entry seemed to have a few race conditions that were hard to resolve. There are a few possible states here we need to account for such as: un-activated, totally not yet running software install cancelled activated but not yet running on the host software install cancelled activated and running on the host software install cancelled before results are completely reported back What I eventually came around to was essentially that we want to mark the SESR cancelled in the same transaction we delete the HSI/UA in. We then finalize it by marking it failed and sending the activity the next time the host fetches setupm experience results. The new cancelled status never leaves fleet. This is a bit ugly but in my testing avoided the race conditions and works well. Note that to actually avoid setup experience hanging entirely we still need to fix #29357 which encompasses several cases where the unified queue can get completely stuck for a host # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes - [ ] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [ ] Added/updated automated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Make sure fleetd is compatible with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)). - [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). - [ ] For unreleased bug fixes in a release candidate, confirmed that the fix is not expected to adversely impact load test results or alerted the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
})
// Add setup_experience_status_results for both installers
API endpoints for Linux setup experience (#32493) For #32040. --- Backend changes to unblock the development of the orbit and frontend changes. New GET and PUT APIs for setting/getting software for Linux Setup Experience: ``` curl -k -X GET -H "Authorization: Bearer $TEST_TOKEN" https://localhost:8080/api/latest/fleet/setup_experience/linux/software?team_id=8&per_page=3000 curl -k -X PUT -H "Authorization: Bearer $TEST_TOKEN" https://localhost:8080/api/latest/fleet/setup_experience/linux/software -d '{"team_id":8,"software_title_ids":[3000, 3001, 3007]}' ``` New setup_experience/init API called by orbit to trigger the Linux setup experience on the device: ``` curl -v -k -X POST -H "Content-Type: application/json" "https://localhost:8080/api/fleet/orbit/setup_experience/init" -d '{"orbit_node_key": "ynYEtFsvv9xZ7rX619UE8of1I28H+GCj"}' ``` Get status API to call on "My device": ``` curl -v -k -X POST "https://localhost:8080/api/latest/fleet/device/7d940b6e-130a-493b-b58a-2b6e9f9f8bfc/setup_experience/status" ``` --- - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [X] Added/updated automated tests - [X] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually ## New Fleet configuration settings - [X] Verified that the setting is exported via `fleetctl generate-gitops` - [X] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [X] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - New Features - Added Linux support for Setup Experience alongside macOS. - Introduced platform-specific admin APIs to configure and retrieve Setup Experience software (macOS/Linux). - Added device API to report Setup Experience status and an Orbit API to initialize Setup Experience on non-macOS devices. - Setup Experience now gates policy queries on Linux until setup is complete. - New activity log entry when Setup Experience software is edited (includes platform and team). - Documentation - Updated audit logs reference to include the new “edited setup experience software” event. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-09-04 15:58:47 +00:00
_, err = ds.EnqueueSetupExperienceItems(ctx, "darwin", host1.UUID, *host1.TeamID)
Mark setup experience installs as "cancelled" and later fail them when certain bulk actions happen (#29355) Still adding tests but wanted to get this up for review of the overall "shape" of the fix When certain things happen like installer updates we delete pending upcoming_activities(UA) and host_software_install(HSI) entries and need to mark setup_experience_status_results(SESR) cancelled. When this happens if that UA/HSI are being depended on by setup experience we need to make sure that that setup experience result eventually gets marked failed. I kind of went back and forth a few times on how best to do this and avoid race conditions. One thing I tried was looking at existence of the UA/HSI but found that naively just trying to look at that in relation to the SESR entry seemed to have a few race conditions that were hard to resolve. There are a few possible states here we need to account for such as: un-activated, totally not yet running software install cancelled activated but not yet running on the host software install cancelled activated and running on the host software install cancelled before results are completely reported back What I eventually came around to was essentially that we want to mark the SESR cancelled in the same transaction we delete the HSI/UA in. We then finalize it by marking it failed and sending the activity the next time the host fetches setupm experience results. The new cancelled status never leaves fleet. This is a bit ugly but in my testing avoided the race conditions and works well. Note that to actually avoid setup experience hanging entirely we still need to fix #29357 which encompasses several cases where the unified queue can get completely stuck for a host # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes - [ ] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [ ] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [ ] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [ ] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [ ] Added/updated automated tests - [ ] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [ ] Make sure fleetd is compatible with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)). - [ ] Orbit runs on macOS, Linux and Windows. Check if the orbit feature/bugfix should only apply to one platform (`runtime.GOOS`). - [ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. - [ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)). - [ ] For unreleased bug fixes in a release candidate, confirmed that the fix is not expected to adversely impact load test results or alerted the release DRI if additional load testing is needed.
2025-05-27 20:52:51 +00:00
require.NoError(t, err)
statuses, err := ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
require.NoError(t, err)
require.Len(t, statuses, 2)
// Enqueue the actual install requests
for _, status := range statuses {
execID, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, *status.SoftwareInstallerID, fleet.HostSoftwareInstallOptions{ForSetupExperience: true})
require.NoError(t, err)
status.HostSoftwareInstallsExecutionID = &execID
status.Status = fleet.SetupExperienceStatusRunning
err = ds.UpdateSetupExperienceStatusResult(ctx, status)
require.NoError(t, err)
}
// batch-set without changes
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install",
InstallerFile: tfr0,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
{
InstallScript: "install",
PostInstallScript: "post-install",
InstallerFile: tfr1,
StorageID: ins1,
Filename: ins1,
Title: ins1,
Source: "apps",
Version: "2",
PreInstallQuery: "select 1 from bar;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example2.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
})
require.NoError(t, err)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
require.NoError(t, err)
require.Len(t, statuses, 2)
for _, status := range statuses {
require.Equal(t, fleet.SetupExperienceStatusRunning, status.Status)
}
// batch-set change ins0's install script to update it and cancel the pending install
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install2",
InstallerFile: tfr0,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
{
InstallScript: "install",
PostInstallScript: "post-install",
InstallerFile: tfr1,
StorageID: ins1,
Filename: ins1,
Title: ins1,
Source: "apps",
Version: "2",
PreInstallQuery: "select 1 from bar;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example2.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
})
require.NoError(t, err)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
require.NoError(t, err)
require.Len(t, statuses, 2)
// Verify that ins0's install was cancelled but ins1 is still running
ins1ExecID := ""
ins0Found := false
ins1Found := false
for _, status := range statuses {
if status.Name == ins0 {
assert.False(t, ins0Found, "duplicate ins0 found")
ins0Found = true
require.Equal(t, fleet.SetupExperienceStatusCancelled, status.Status)
} else {
assert.False(t, ins1Found, "duplicate ins1 found")
assert.Equal(t, ins1, status.Name)
require.Equal(t, fleet.SetupExperienceStatusRunning, status.Status)
require.NotNil(t, status.HostSoftwareInstallsExecutionID)
ins1ExecID = *status.HostSoftwareInstallsExecutionID
}
}
// activate and set a result for ins1 as if the install completed
ds.testActivateSpecificNextActivities = []string{ins1ExecID}
_, err = ds.activateNextUpcomingActivity(ctx, ds.writer(ctx), host1.ID, "")
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host1.ID,
InstallUUID: ins1ExecID,
InstallScriptExitCode: ptr.Int(0),
})
require.NoError(t, err)
// batch-set change ins1's install script to update it. This should do nothing to the setup
// experience result because the install already completed
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
{
InstallScript: "install2",
InstallerFile: tfr0,
StorageID: ins0,
Filename: ins0,
Title: ins0,
Source: "apps",
Version: "1",
PreInstallQuery: "select 0 from foo;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
{
InstallScript: "install3",
PostInstallScript: "post-install",
InstallerFile: tfr1,
StorageID: ins1,
Filename: ins1,
Title: ins1,
Source: "apps",
Version: "2",
PreInstallQuery: "select 1 from bar;",
UserID: user1.ID,
Platform: "darwin",
URL: "https://example2.com",
InstallDuringSetup: ptr.Bool(true),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
},
})
require.NoError(t, err)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
require.NoError(t, err)
require.Len(t, statuses, 2)
// Verify that ins0's install is still cancelled and ins1 is still running(because it hasn't
// been updated in the SESR entry yet)
ins0Found = false
ins1Found = false
for _, status := range statuses {
if status.Name == ins0 {
assert.False(t, ins0Found, "duplicate ins0 found")
ins0Found = true
require.Equal(t, fleet.SetupExperienceStatusCancelled, status.Status)
} else {
assert.False(t, ins1Found, "duplicate ins1 found")
assert.Equal(t, ins1, status.Name)
require.Equal(t, fleet.SetupExperienceStatusRunning, status.Status)
}
}
}
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 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, errDeleteInstallerWithAssociatedPolicy)
_, 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),
})
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)
2024-09-04 21:46:48 +00:00
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),
})
require.NoError(t, err)
// Last installation should be "installed".
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
require.NoError(t, err)
require.NotNil(t, host1LastInstall)
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
require.NotNil(t, host1LastInstall.Status)
2024-09-04 21:46:48 +00:00
require.Equal(t, fleet.SoftwareInstalled, *host1LastInstall.Status)
// 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)
2024-09-04 21:46:48 +00:00
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)
2024-09-04 21:46:48 +00:00
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)
2024-09-04 21:46:48 +00:00
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),
})
require.NoError(t, err)
_, err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host1.ID,
InstallUUID: installUUID3,
InstallScriptExitCode: ptr.Int(1),
})
require.NoError(t, err)
// Last installation for installer1.pkg should be "failed".
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID1)
require.NoError(t, err)
require.NotNil(t, host1LastInstall)
require.Equal(t, installUUID3, host1LastInstall.ExecutionID)
require.NotNil(t, host1LastInstall.Status)
2024-09-04 21:46:48 +00:00
require.Equal(t, fleet.SoftwareInstallFailed, *host1LastInstall.Status)
// 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())
software1 := []fleet.Software{
{Name: "Existing Title", Version: "0.0.1", Source: "apps", BundleIdentifier: "existing.title"},
}
software2 := []fleet.Software{
{Name: "Existing Title", Version: "v0.0.2", Source: "apps", BundleIdentifier: "existing.title"},
{Name: "Existing Title", Version: "0.0.3", Source: "apps", BundleIdentifier: "existing.title"},
{Name: "Existing Title Without Bundle", Version: "0.0.3", Source: "apps"},
}
_, err := ds.UpdateHostSoftware(ctx, host1.ID, software1)
require.NoError(t, err)
_, err = ds.UpdateHostSoftware(ctx, host2.ID, software2)
require.NoError(t, err)
require.NoError(t, ds.SyncHostsSoftware(ctx, time.Now()))
require.NoError(t, ds.SyncHostsSoftwareTitles(ctx, time.Now()))
tests := []struct {
name string
payload *fleet.UploadSoftwareInstallerPayload
}{
{
name: "title that already exists, no bundle identifier in payload",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "Existing Title",
Source: "apps",
},
},
{
name: "title that already exists, mismatched bundle identifier in payload",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "Existing Title",
Source: "apps",
BundleIdentifier: "com.existing.bundle",
},
},
{
name: "title that already exists but doesn't have a bundle identifier",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "Existing Title Without Bundle",
Source: "apps",
},
},
{
name: "title that already exists, no bundle identifier in DB, bundle identifier in payload",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "Existing Title Without Bundle",
Source: "apps",
BundleIdentifier: "com.new.bundleid",
},
},
{
name: "title that doesn't exist, no bundle identifier in payload",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "New Title",
Source: "some_source",
},
},
{
name: "title that doesn't exist, with bundle identifier in payload",
payload: &fleet.UploadSoftwareInstallerPayload{
Title: "New Title With Bundle",
Source: "some_source",
BundleIdentifier: "com.new.bundle",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
id, err := ds.getOrGenerateSoftwareInstallerTitleID(ctx, tt.payload)
require.NoError(t, err)
require.NotEmpty(t, id)
})
}
}
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),
})
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),
})
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)
}
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 testSaveInstallerUpdatesClearsFleetMaintainedAppID(t *testing.T, ds *Datastore) {
ctx := context.Background()
user := test.NewUser(t, ds, "Test User", "test@example.com", true)
tfr, err := fleet.NewTempFileReader(strings.NewReader("file contents"), t.TempDir)
require.NoError(t, err)
// Create a maintained app
maintainedApp, err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
Name: "Maintained1",
Slug: "maintained1",
Platform: "darwin",
UniqueIdentifier: "fleet.maintained1",
})
require.NoError(t, err)
// Create an installer with a non-NULL fleet_maintained_app_id
installerID, titleID, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
Title: "testpkg",
Source: "apps",
InstallScript: "echo install",
PreInstallQuery: "SELECT 1",
UninstallScript: "echo uninstall",
InstallerFile: tfr,
StorageID: "storageid1",
Filename: "test.pkg",
Version: "1.0",
UserID: user.ID,
ValidatedLabels: &fleet.LabelIdentsWithScope{},
FleetMaintainedAppID: ptr.Uint(maintainedApp.ID),
})
require.NoError(t, err)
// Prepare update payload with a new installer file (should clear FMA id)
installScript := "echo install updated"
uninstallScript := "echo uninstall updated"
preInstallQuery := "SELECT 2"
selfService := true
payload := &fleet.UpdateSoftwareInstallerPayload{
TitleID: titleID,
InstallerID: installerID,
StorageID: "storageid2", // different storage id
Filename: "test2.pkg",
Version: "2.0",
PackageIDs: []string{"com.test.pkg"},
InstallScript: &installScript,
UninstallScript: &uninstallScript,
PreInstallQuery: &preInstallQuery,
SelfService: &selfService,
InstallerFile: tfr, // triggers clearing
UserID: user.ID,
}
require.NoError(t, ds.SaveInstallerUpdates(ctx, payload))
// Assert that fleet_maintained_app_id is now NULL
var fmaID *uint
err = sqlx.GetContext(ctx, ds.reader(ctx), &fmaID, `SELECT fleet_maintained_app_id FROM software_installers WHERE id = ?`, installerID)
require.NoError(t, err)
assert.Nil(t, fmaID, "fleet_maintained_app_id should be NULL after update")
}
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 team "Team A".`, "expected existsError for same-team duplicate title, got: %T: %v", err, err)
}