incorporate display name into setup experience ordering and enforce 1 at a time execution (#42393)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41741 

# 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.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## 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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Software setup items are now ordered using custom display names when
available.

* **Bug Fixes**
* Software installations now process sequentially for improved
reliability and predictability.
* Enhanced handling of missing installation tracking data to prevent
failures.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
Jahziel Villasana-Espinoza 2026-04-06 12:51:39 -04:00 committed by GitHub
parent 344e4f2dcd
commit 1b95a581f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 697 additions and 267 deletions

1
changes/41741-order Normal file
View file

@ -0,0 +1 @@
- Updated ordering of setup experience software to take display names into account.

View file

@ -12,6 +12,7 @@ import (
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
func (svc *Service) ListDevicePolicies(ctx context.Context, host *fleet.Host) ([]*fleet.HostPolicy, error) {
@ -313,7 +314,7 @@ func (svc *Service) getHostSetupExperienceStatus(ctx context.Context, host *flee
}
// Get current status of the setup experience.
results, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
results, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, ptr.ValOrZero(host.TeamID))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
}

View file

@ -129,7 +129,7 @@ func (svc *Service) GetOrbitSetupExperienceStatus(ctx context.Context, orbitNode
}
// get status of software installs and script execution
res, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
res, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID, ptr.ValOrZero(host.TeamID))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
}
@ -153,19 +153,15 @@ func (svc *Service) GetOrbitSetupExperienceStatus(ctx context.Context, orbitNode
// then re-enqueue any cancelled setup experience steps.
if hasFailedSoftwareInstall {
if resetFailedSetupSteps {
teamID := uint(0)
if host.TeamID != nil {
teamID = *host.TeamID
}
// If so, call the enqueue function with a flag to retain successful steps.
if requireAllSoftware {
svc.logger.InfoContext(ctx, "re-enqueueing cancelled setup experience steps after a previous software install failure", "host_uuid", host.UUID)
_, err := svc.ds.ResetSetupExperienceItemsAfterFailure(ctx, host.Platform, host.PlatformLike, host.UUID, teamID)
_, err := svc.ds.ResetSetupExperienceItemsAfterFailure(ctx, host.Platform, host.PlatformLike, host.UUID, ptr.ValOrZero(host.TeamID))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "re-enqueueing cancelled setup experience steps after a previous software install failure")
}
// Re-fetch the setup experience results after re-enqueuing.
res, err = svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
res, err = svc.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID, ptr.ValOrZero(host.TeamID))
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "listing setup experience results")
}
@ -277,7 +273,7 @@ func (svc *Service) failCancelledSetupExperienceInstalls(
HostDisplayName: hostDisplayName,
SoftwareTitle: r.Name,
SoftwarePackage: softwarePackage,
InstallUUID: *r.HostSoftwareInstallsExecutionID,
InstallUUID: ptr.ValOrZero(r.HostSoftwareInstallsExecutionID),
Status: "failed",
SelfService: false,
Source: source,

View file

@ -186,13 +186,19 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Hos
if err != nil {
return false, ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
}
statuses, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
statuses, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, ptr.ValOrZero(host.TeamID))
if err != nil {
return false, ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step")
}
var installersPending, appsPending, scriptsPending []*fleet.SetupExperienceStatusResult
var installersRunning, appsRunning, scriptsRunning int
// Software (installers and VPP apps) are treated as a single group,
// ordered alphabetically by display name (falling back to name). This
// ordering is determined at enqueue time by enqueueSetupExperienceItems,
// which inserts them with auto-incremented IDs in the correct order.
// ListSetupExperienceResultsByHostUUID returns rows ordered by sesr.id.
// Scripts always run after all software is done.
var softwarePending, scriptsPending []*fleet.SetupExperienceStatusResult
var softwareRunning, scriptsRunning int
for _, status := range statuses {
if err := status.IsValid(); err != nil {
@ -200,21 +206,14 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Hos
}
switch {
case status.SoftwareInstallerID != nil:
case status.IsForSoftware():
switch status.Status {
case fleet.SetupExperienceStatusPending:
installersPending = append(installersPending, status)
softwarePending = append(softwarePending, status)
case fleet.SetupExperienceStatusRunning:
installersRunning++
softwareRunning++
}
case status.VPPAppTeamID != nil:
switch status.Status {
case fleet.SetupExperienceStatusPending:
appsPending = append(appsPending, status)
case fleet.SetupExperienceStatusRunning:
appsRunning++
}
case status.SetupExperienceScriptID != nil:
case status.IsForScript():
switch status.Status {
case fleet.SetupExperienceStatusPending:
scriptsPending = append(scriptsPending, status)
@ -225,38 +224,41 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Hos
}
switch {
case len(installersPending) > 0:
// enqueue installers
for _, installer := range installersPending {
installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, fleet.HostSoftwareInstallOptions{
case len(softwarePending) > 0 && softwareRunning == 0:
// Enqueue only the first pending software item (installer or VPP app).
// On the next call, this item will be in "running" state and the next
// pending item will be picked up. This ensures software is installed
// one at a time in the alphabetical display-name order determined at
// enqueue time (rows are ordered by sesr.id).
sw := softwarePending[0]
switch {
case sw.SoftwareInstallerID != nil:
installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *sw.SoftwareInstallerID, fleet.HostSoftwareInstallOptions{
SelfService: false,
ForSetupExperience: true,
})
if err != nil {
return false, ctxerr.Wrap(ctx, err, "queueing setup experience install request")
}
installer.HostSoftwareInstallsExecutionID = &installUUID
installer.Status = fleet.SetupExperienceStatusRunning
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, installer); err != nil {
sw.HostSoftwareInstallsExecutionID = &installUUID
sw.Status = fleet.SetupExperienceStatusRunning
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, sw); err != nil {
return false, ctxerr.Wrap(ctx, err, "updating setup experience result with install uuid")
}
}
case installersRunning == 0 && len(appsPending) > 0:
// enqueue vpp apps
var skipRemainingVPPInstalls bool
enqueueVPPApps:
for _, app := range appsPending {
vppAppID, err := app.VPPAppID()
case sw.VPPAppTeamID != nil:
vppAppID, err := sw.VPPAppID()
if err != nil {
return false, ctxerr.Wrap(ctx, err, "constructing vpp app details for installation")
}
if app.SoftwareTitleID == nil {
return false, ctxerr.Errorf(ctx, "setup experience software title id missing from vpp app install request: %d", app.ID)
if sw.SoftwareTitleID == nil {
return false, ctxerr.Errorf(ctx, "setup experience software title id missing from vpp app install request: %d", sw.ID)
}
vppApp := &fleet.VPPApp{
TitleID: *app.SoftwareTitleID,
TitleID: *sw.SoftwareTitleID,
VPPAppTeam: fleet.VPPAppTeam{
VPPAppID: *vppAppID,
},
@ -267,16 +269,13 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Hos
ForSetupExperience: true,
})
app.NanoCommandUUID = &cmdUUID
app.Status = fleet.SetupExperienceStatusRunning
if err != nil {
// if we get an error (e.g. no available licenses) while attempting to enqueue the
// install, then we should immediately go to an error state so setup experience
// isn't blocked.
svc.logger.WarnContext(ctx, "got an error when attempting to enqueue VPP app install", "err", err, "adam_id", app.VPPAppAdamID)
app.Status = fleet.SetupExperienceStatusFailure
app.Error = ptr.String(err.Error())
svc.logger.WarnContext(ctx, "got an error when attempting to enqueue VPP app install", "err", err, "adam_id", sw.VPPAppAdamID)
sw.Status = fleet.SetupExperienceStatusFailure
sw.Error = ptr.String(err.Error())
// At this point we need to check whether the "cancel if software install fails" setting is active,
// in which case we'll cancel the remaining pending items.
requireAllSoftware, err := svc.IsAllSetupExperienceSoftwareRequired(ctx, host)
@ -288,17 +287,16 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Hos
if err != nil {
return false, ctxerr.Wrap(ctx, err, "cancelling remaining setup experience steps after vpp app install failure")
}
skipRemainingVPPInstalls = true
}
} else {
sw.NanoCommandUUID = &cmdUUID
sw.Status = fleet.SetupExperienceStatusRunning
}
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, app); err != nil {
if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, sw); err != nil {
return false, ctxerr.Wrap(ctx, err, "updating setup experience with vpp install command uuid")
}
if skipRemainingVPPInstalls {
break enqueueVPPApps
}
}
case installersRunning == 0 && appsRunning == 0 && len(scriptsPending) > 0:
case softwareRunning == 0 && len(scriptsPending) > 0:
// enqueue scripts
for _, script := range scriptsPending {
if script.ScriptContentID == nil {
@ -323,7 +321,7 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Hos
return false, ctxerr.Wrap(ctx, err, "updating setup experience script execution id")
}
}
case installersRunning == 0 && appsRunning == 0 && scriptsRunning == 0:
case softwareRunning == 0 && scriptsRunning == 0:
// finished
return true, nil
}

View file

@ -52,7 +52,7 @@ func TestSetupExperienceNextStep(t *testing.T) {
}
var mockListSetupExperience []*fleet.SetupExperienceStatusResult
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) {
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) {
return mockListSetupExperience, nil
}

View file

@ -97,21 +97,29 @@ WHERE host_uuid = ? AND %s`
stmtClearSetupStatus = fmt.Sprintf(stmtClearSetupStatus, "TRUE")
}
// stmtSoftwareInstallers query currently supports installers for macOS and Linux.
stmtSoftwareInstallers := `
INSERT INTO setup_experience_status_results (
host_uuid,
name,
status,
software_installer_id
) SELECT
?,
st.name,
'pending',
si.id
// Build combined software query (installers + VPP apps) before the transaction.
fleetPlatform := fleet.PlatformFromHost(hostPlatformLike)
var softwareUnionParts []string
var softwareArgs []any
includeSoftwareInstallers := fleetPlatform != "ios" && fleetPlatform != "ipados"
includeVPPApps := fleetPlatform == "darwin" || fleetPlatform == "ios" || fleetPlatform == "ipados"
if includeSoftwareInstallers {
installerSelect := `
SELECT
? AS host_uuid,
st.name AS name,
'pending' AS status,
si.id AS software_installer_id,
NULL AS vpp_app_team_id,
COALESCE(stdn.display_name, st.name) AS sort_name
FROM software_installers si
INNER JOIN software_titles st
ON si.title_id = st.id
LEFT JOIN software_title_display_names stdn
ON stdn.software_title_id = st.id AND stdn.team_id = ?
WHERE install_during_setup = true
AND global_or_team_id = ?
AND si.is_active = TRUE
@ -138,41 +146,66 @@ AND (
)
)
)
AND %s ORDER BY st.name ASC
`
if resetFailedSetupSteps {
stmtSoftwareInstallers = fmt.Sprintf(stmtSoftwareInstallers, "si.id NOT IN (SELECT software_installer_id FROM setup_experience_status_results WHERE host_uuid = ? AND status = 'success' AND software_installer_id IS NOT NULL)")
} else {
stmtSoftwareInstallers = fmt.Sprintf(stmtSoftwareInstallers, "TRUE")
AND %s`
if resetFailedSetupSteps {
installerSelect = fmt.Sprintf(installerSelect, "si.id NOT IN (SELECT software_installer_id FROM setup_experience_status_results WHERE host_uuid = ? AND status = 'success' AND software_installer_id IS NOT NULL)")
} else {
installerSelect = fmt.Sprintf(installerSelect, "TRUE")
}
softwareUnionParts = append(softwareUnionParts, installerSelect)
softwareArgs = append(softwareArgs, hostUUID, teamID, teamID, fleetPlatform, hostPlatformLike, hostPlatformLike)
if resetFailedSetupSteps {
softwareArgs = append(softwareArgs, hostUUID)
}
}
stmtVPPApps := `
INSERT INTO setup_experience_status_results (
host_uuid,
name,
status,
vpp_app_team_id
) SELECT
?,
st.name,
'pending',
vat.id
if includeVPPApps {
vppSelect := `
SELECT
? AS host_uuid,
st.name AS name,
'pending' AS status,
NULL AS software_installer_id,
vat.id AS vpp_app_team_id,
COALESCE(stdn.display_name, st.name) AS sort_name
FROM vpp_apps va
INNER JOIN vpp_apps_teams vat
ON vat.adam_id = va.adam_id
AND vat.platform = va.platform
INNER JOIN software_titles st
ON va.title_id = st.id
LEFT JOIN software_title_display_names stdn
ON stdn.software_title_id = st.id AND stdn.team_id = ?
WHERE vat.install_during_setup = true
AND vat.global_or_team_id = ?
AND va.platform = ?
AND %s
ORDER BY st.name ASC
`
if resetFailedSetupSteps {
stmtVPPApps = fmt.Sprintf(stmtVPPApps, "vat.id NOT IN (SELECT vpp_app_team_id FROM setup_experience_status_results WHERE host_uuid = ? AND status = 'success' AND vpp_app_team_id IS NOT NULL)")
} else {
stmtVPPApps = fmt.Sprintf(stmtVPPApps, "TRUE")
AND %s`
if resetFailedSetupSteps {
vppSelect = fmt.Sprintf(vppSelect, "vat.id NOT IN (SELECT vpp_app_team_id FROM setup_experience_status_results WHERE host_uuid = ? AND status = 'success' AND vpp_app_team_id IS NOT NULL)")
} else {
vppSelect = fmt.Sprintf(vppSelect, "TRUE")
}
softwareUnionParts = append(softwareUnionParts, vppSelect)
softwareArgs = append(softwareArgs, hostUUID, teamID, teamID, fleetPlatform)
if resetFailedSetupSteps {
softwareArgs = append(softwareArgs, hostUUID)
}
}
var stmtSoftwareCombined string
if len(softwareUnionParts) > 0 {
stmtSoftwareCombined = fmt.Sprintf(`
INSERT INTO setup_experience_status_results (
host_uuid,
name,
status,
software_installer_id,
vpp_app_team_id
)
SELECT host_uuid, name, status, software_installer_id, vpp_app_team_id FROM (
%s
) AS combined
ORDER BY sort_name ASC, COALESCE(software_installer_id, vpp_app_team_id, 0)`, strings.Join(softwareUnionParts, " UNION ALL "))
}
stmtSetupScripts := `
@ -198,37 +231,15 @@ WHERE global_or_team_id = ?`
return ctxerr.Wrap(ctx, err, "removing stale setup experience entries")
}
// Software installers
fleetPlatform := fleet.PlatformFromHost(hostPlatformLike)
args := []any{hostUUID, teamID, fleetPlatform, hostPlatformLike, hostPlatformLike}
if resetFailedSetupSteps {
args = append(args, hostUUID)
}
if fleetPlatform != "ios" && fleetPlatform != "ipados" {
res, err := tx.ExecContext(ctx, stmtSoftwareInstallers, args...)
// Combined software (installers + VPP apps)
if stmtSoftwareCombined != "" {
res, err := tx.ExecContext(ctx, stmtSoftwareCombined, softwareArgs...)
if err != nil {
return ctxerr.Wrap(ctx, err, "inserting setup experience software installers")
return ctxerr.Wrap(ctx, err, "inserting setup experience software items")
}
inserts, err := res.RowsAffected()
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving number of inserted software installers")
}
totalInsertions += uint(inserts) // nolint: gosec
}
// VPP apps
if fleetPlatform == "darwin" || fleetPlatform == "ios" || fleetPlatform == "ipados" {
args := []any{hostUUID, teamID, fleetPlatform}
if resetFailedSetupSteps {
args = append(args, hostUUID)
}
res, err := tx.ExecContext(ctx, stmtVPPApps, args...)
if err != nil {
return ctxerr.Wrap(ctx, err, "inserting setup experience vpp apps")
}
inserts, err := res.RowsAffected()
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving number of inserted vpp apps")
return ctxerr.Wrap(ctx, err, "retrieving number of inserted software items")
}
totalInsertions += uint(inserts) // nolint: gosec
}
@ -531,7 +542,7 @@ func questionMarks(number int) string {
return strings.Join(slices.Repeat([]string{"?"}, number), ",")
}
func (ds *Datastore) ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) {
func (ds *Datastore) ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) {
const stmt = `
SELECT
sesr.id,
@ -571,6 +582,7 @@ LEFT JOIN host_script_results hsr ON hsr.execution_id = sesr.script_execution_id
LEFT JOIN vpp_apps_teams vat ON vat.id = sesr.vpp_app_team_id
LEFT JOIN vpp_apps va ON vat.adam_id = va.adam_id AND vat.platform = va.platform
WHERE host_uuid = ?
ORDER BY sesr.id
`
var results []*fleet.SetupExperienceStatusResult
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, hostUUID); err != nil {
@ -588,28 +600,12 @@ WHERE host_uuid = ?
// load custom display name and custom icon for the software installers, if any
if len(titleIDs) > 0 {
// NOTE: as documented in fleet.HostUUIDForSetupExperience, the setup experience "host_uuid"
// is NOT always the host.uuid (on Windows and Linux, specifically). So if the host's team is
// not found, we simply don't load the icons and display names, anyway we only need those
// on macOS currently as it's the only place where the setup experience UI is shown.
// we need the host's team to load the custom icons and display names
const hostTeam = `SELECT team_id FROM hosts WHERE uuid = ? LIMIT 1`
var hostTeamID sql.Null[uint]
if err := sqlx.GetContext(ctx, ds.reader(ctx), &hostTeamID, hostTeam, hostUUID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
// host not found, skip loading icons and display names
return results, nil
}
return nil, ctxerr.Wrap(ctx, err, "get host team ID for setup experience results")
}
icons, err := ds.GetSoftwareIconsByTeamAndTitleIds(ctx, hostTeamID.V, titleIDs)
icons, err := ds.GetSoftwareIconsByTeamAndTitleIds(ctx, teamID, titleIDs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get software icons by team and title IDs")
}
displayNames, err := ds.getDisplayNamesByTeamAndTitleIds(ctx, hostTeamID.V, titleIDs)
displayNames, err := ds.getDisplayNamesByTeamAndTitleIds(ctx, teamID, titleIDs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get software display names by team and title IDs")
}

View file

@ -33,6 +33,7 @@ func TestSetupExperience(t *testing.T) {
{"TestGetSetupExperienceScriptByID", testGetSetupExperienceScriptByID},
{"TestUpdateSetupExperienceScriptWhileEnqueued", testUpdateSetupExperienceScriptWhileEnqueued},
{"TestEnqueueSetupExperienceItemsWindows", testEnqueueSetupExperienceItemsWindows},
{"EnqueueSetupExperienceItemsWithDisplayName", testEnqueueSetupExperienceItemsWithDisplayName},
}
for _, c := range cases {
@ -666,6 +667,271 @@ func testEnqueueSetupExperienceItems(t *testing.T, ds *Datastore) {
}
}
// testEnqueueSetupExperienceItemsWithDisplayName verifies that when a custom
// display name is set for a software title, the enqueue function uses it to
// determine the alphabetical install order (instead of the default
// software_titles.name). This ordering also orders the steps in the
// setup experience UI. The UI uses the display name if it is set, and
// the name if not.
func testEnqueueSetupExperienceItemsWithDisplayName(t *testing.T, ds *Datastore) {
ctx := context.Background()
test.CreateInsertGlobalVPPToken(t, ds)
team, err := ds.NewTeam(ctx, &fleet.Team{Name: "team_display_name_test"})
require.NoError(t, err)
user := test.NewUser(t, ds, "DisplayNameUser", "displaynameuser@example.com", true)
// Create two software installers with titles that sort in a known order:
// "AAA_Software" < "ZZZ_Software" (alphabetically)
// We will then assign custom display names that invert this order:
// "AAA_Software" → "Zulu Custom"
// "ZZZ_Software" → "Alpha Custom"
// After enqueue, the rows ordered by id (insert order) should reflect
// the display-name alphabetical order:
// id=N → ZZZ_Software (display name "Alpha Custom", sorts first)
// id=N+1 → AAA_Software (display name "Zulu Custom", sorts second)
// But the `name` column still stores the original st.name.
// Note that the setup experience UI will also follow this ordering;
// it will display "Alpha Custom" and then "Zulu Custom".
tfr1, err := fleet.NewTempFileReader(strings.NewReader("hello1"), t.TempDir)
require.NoError(t, err)
installerID1, titleID1, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install1",
UninstallScript: "uninstall1",
InstallerFile: tfr1,
StorageID: "storage_dn_1",
Filename: "file_dn_1",
Title: "AAA_Software",
Version: "1.0",
Source: "apps",
UserID: user.ID,
TeamID: &team.ID,
Platform: string(fleet.MacOSPlatform),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
tfr2, err := fleet.NewTempFileReader(strings.NewReader("hello2"), t.TempDir)
require.NoError(t, err)
installerID2, titleID2, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install2",
UninstallScript: "uninstall2",
InstallerFile: tfr2,
StorageID: "storage_dn_2",
Filename: "file_dn_2",
Title: "ZZZ_Software",
Version: "2.0",
Source: "apps",
UserID: user.ID,
TeamID: &team.ID,
Platform: string(fleet.MacOSPlatform),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// Mark both installers for setup experience
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, "UPDATE software_installers SET install_during_setup = 1 WHERE id IN (?, ?)", installerID1, installerID2)
return err
})
// Set custom display names that invert the alphabetical order
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
if err := updateSoftwareTitleDisplayName(ctx, q, &team.ID, titleID1, "Zulu Custom"); err != nil {
return err
}
return updateSoftwareTitleDisplayName(ctx, q, &team.ID, titleID2, "Alpha Custom")
})
// Create two VPP apps with titles that sort in a known order, then invert with display names.
vppApp1 := &fleet.VPPApp{
Name: "AAA_VPP_App",
BundleIdentifier: "com.aaa.vpp",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "dn_adam_1", Platform: fleet.MacOSPlatform}},
}
vpp1, err := ds.InsertVPPAppWithTeam(ctx, vppApp1, &team.ID)
require.NoError(t, err)
vppApp2 := &fleet.VPPApp{
Name: "ZZZ_VPP_App",
BundleIdentifier: "com.zzz.vpp",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "dn_adam_2", Platform: fleet.MacOSPlatform}},
}
vpp2, err := ds.InsertVPPAppWithTeam(ctx, vppApp2, &team.ID)
require.NoError(t, err)
// Mark both VPP apps for setup experience
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, "UPDATE vpp_apps_teams SET install_during_setup = 1 WHERE adam_id IN (?, ?)", vpp1.AdamID, vpp2.AdamID)
return err
})
// Set custom display names for VPP apps (invert order)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
if err := updateSoftwareTitleDisplayName(ctx, q, &team.ID, vppApp1.TitleID, "Zulu VPP Custom"); err != nil {
return err
}
return updateSoftwareTitleDisplayName(ctx, q, &team.ID, vppApp2.TitleID, "Alpha VPP Custom")
})
// Create a host assigned to the team and enqueue setup experience.
// The host must be on the team so that ListSetupExperienceResultsByHostUUID
// can look up the team's display names.
hostUUID := "host-display-name-test-" + uuid.NewString()
host1, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-dn-test",
OsqueryHostID: ptr.String("osquery-dn-test"),
NodeKey: ptr.String("node-key-dn-test"),
UUID: hostUUID,
Platform: "darwin",
HardwareSerial: "dn-serial-1",
})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host1.ID}))
require.NoError(t, err)
anythingEnqueued, err := ds.EnqueueSetupExperienceItems(ctx, "darwin", "darwin", hostUUID, team.ID)
require.NoError(t, err)
require.True(t, anythingEnqueued)
// --- Verify all rows are globally ordered by display name ---
// enqueueSetupExperienceItems inserts software (installers and VPP apps)
// together in a single query ordered by COALESCE(display_name, st.name),
// so the auto-incremented id reflects the global display-name order.
// ListSetupExperienceResultsByHostUUID returns rows ordered by sesr.id,
// preserving that insert order. Scripts are inserted last.
//
// Expected order (all software globally sorted by display name):
// 0. ZZZ_Software (installer, display name "Alpha Custom")
// 1. ZZZ_VPP_App (VPP app, display name "Alpha VPP Custom")
// 2. AAA_Software (installer, display name "Zulu Custom")
// 3. AAA_VPP_App (VPP app, display name "Zulu VPP Custom")
allResults, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, team.ID)
require.NoError(t, err)
require.Len(t, allResults, 4, "expected 4 results total (2 installers + 2 VPP apps)")
assert.Equal(t, "ZZZ_Software", allResults[0].Name, "row 0: ZZZ_Software (display name 'Alpha Custom')")
assert.Equal(t, "Alpha Custom", allResults[0].DisplayName, "row 0: display name should be 'Alpha Custom'")
assert.NotNil(t, allResults[0].SoftwareInstallerID, "row 0: should be a software installer")
assert.Equal(t, "ZZZ_VPP_App", allResults[1].Name, "row 1: ZZZ_VPP_App (display name 'Alpha VPP Custom')")
assert.Equal(t, "Alpha VPP Custom", allResults[1].DisplayName, "row 1: display name should be 'Alpha VPP Custom'")
assert.NotNil(t, allResults[1].VPPAppTeamID, "row 1: should be a VPP app")
assert.Less(t, allResults[0].ID, allResults[1].ID)
assert.Equal(t, "AAA_Software", allResults[2].Name, "row 2: AAA_Software (display name 'Zulu Custom')")
assert.Equal(t, "Zulu Custom", allResults[2].DisplayName, "row 2: display name should be 'Zulu Custom'")
assert.NotNil(t, allResults[2].SoftwareInstallerID, "row 2: should be a software installer")
assert.Less(t, allResults[1].ID, allResults[2].ID)
assert.Equal(t, "AAA_VPP_App", allResults[3].Name, "row 3: AAA_VPP_App (display name 'Zulu VPP Custom')")
assert.Equal(t, "Zulu VPP Custom", allResults[3].DisplayName, "row 3: display name should be 'Zulu VPP Custom'")
assert.NotNil(t, allResults[3].VPPAppTeamID, "row 3: should be a VPP app")
// --- Verify fallback: no display name → order uses st.name ---
// Add a third installer and a third VPP app, both without custom display
// names, then re-enqueue for a new host and verify the globally
// interleaved order. Items without a display name fall back to st.name.
tfr3, err := fleet.NewTempFileReader(strings.NewReader("hello3"), t.TempDir)
require.NoError(t, err)
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
InstallScript: "install3",
UninstallScript: "uninstall3",
InstallerFile: tfr3,
StorageID: "storage_dn_3",
Filename: "file_dn_3",
Title: "MMM_NoDisplayName",
Version: "3.0",
Source: "apps",
UserID: user.ID,
TeamID: &team.ID,
Platform: string(fleet.MacOSPlatform),
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, "UPDATE software_installers SET install_during_setup = 1 WHERE id NOT IN (?, ?)", installerID1, installerID2)
return err
})
vppApp3 := &fleet.VPPApp{
Name: "MMM_VPP_NoDisplayName",
BundleIdentifier: "com.mmm.vpp",
VPPAppTeam: fleet.VPPAppTeam{VPPAppID: fleet.VPPAppID{AdamID: "dn_adam_3", Platform: fleet.MacOSPlatform}},
}
vpp3, err := ds.InsertVPPAppWithTeam(ctx, vppApp3, &team.ID)
require.NoError(t, err)
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, "UPDATE vpp_apps_teams SET install_during_setup = 1 WHERE adam_id = ?", vpp3.AdamID)
return err
})
// Re-enqueue for a new host (also on the team) to pick up all installers and VPP apps.
hostUUID2 := "host-display-name-fallback-" + uuid.NewString()
host2, err := ds.NewHost(ctx, &fleet.Host{
Hostname: "macos-dn-test-2",
OsqueryHostID: ptr.String("osquery-dn-test-2"),
NodeKey: ptr.String("node-key-dn-test-2"),
UUID: hostUUID2,
Platform: "darwin",
HardwareSerial: "dn-serial-2",
})
require.NoError(t, err)
err = ds.AddHostsToTeam(ctx, fleet.NewAddHostsToTeamParams(&team.ID, []uint{host2.ID}))
require.NoError(t, err)
anythingEnqueued, err = ds.EnqueueSetupExperienceItems(ctx, "darwin", "darwin", hostUUID2, team.ID)
require.NoError(t, err)
require.True(t, anythingEnqueued)
// Verify the globally interleaved order across installers and VPP apps.
// The combined INSERT in enqueueSetupExperienceItems orders by
// COALESCE(display_name, st.name), and ListSetupExperienceResultsByHostUUID
// returns rows ordered by sesr.id (i.e. insert order).
//
// Expected global order (sorted by COALESCE(display_name, st.name)):
// 0. ZZZ_Software (installer, display name "Alpha Custom")
// 1. ZZZ_VPP_App (VPP app, display name "Alpha VPP Custom")
// 2. MMM_NoDisplayName (installer, no display name → falls back to st.name)
// 3. MMM_VPP_NoDisplayName (VPP app, no display name → falls back to st.name)
// 4. AAA_Software (installer, display name "Zulu Custom")
// 5. AAA_VPP_App (VPP app, display name "Zulu VPP Custom")
fallbackResults, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID2, team.ID)
require.NoError(t, err)
require.Len(t, fallbackResults, 6, "expected 6 results total (3 installers + 3 VPP apps)")
assert.Equal(t, "ZZZ_Software", fallbackResults[0].Name, "row 0: ZZZ_Software (display name 'Alpha Custom')")
assert.Equal(t, "Alpha Custom", fallbackResults[0].DisplayName)
assert.NotNil(t, fallbackResults[0].SoftwareInstallerID)
assert.Equal(t, "ZZZ_VPP_App", fallbackResults[1].Name, "row 1: ZZZ_VPP_App (display name 'Alpha VPP Custom')")
assert.Equal(t, "Alpha VPP Custom", fallbackResults[1].DisplayName)
assert.NotNil(t, fallbackResults[1].VPPAppTeamID)
assert.Less(t, fallbackResults[0].ID, fallbackResults[1].ID)
assert.Equal(t, "MMM_NoDisplayName", fallbackResults[2].Name, "row 2: MMM_NoDisplayName (no display name, falls back to st.name)")
assert.Empty(t, fallbackResults[2].DisplayName)
assert.NotNil(t, fallbackResults[2].SoftwareInstallerID)
assert.Less(t, fallbackResults[1].ID, fallbackResults[2].ID)
assert.Equal(t, "MMM_VPP_NoDisplayName", fallbackResults[3].Name, "row 3: MMM_VPP_NoDisplayName (no display name, falls back to st.name)")
assert.Empty(t, fallbackResults[3].DisplayName)
assert.NotNil(t, fallbackResults[3].VPPAppTeamID)
assert.Equal(t, "AAA_Software", fallbackResults[4].Name, "row 4: AAA_Software (display name 'Zulu Custom')")
assert.Equal(t, "Zulu Custom", fallbackResults[4].DisplayName)
assert.NotNil(t, fallbackResults[4].SoftwareInstallerID)
assert.Equal(t, "AAA_VPP_App", fallbackResults[5].Name, "row 5: AAA_VPP_App (display name 'Zulu VPP Custom')")
assert.Equal(t, "Zulu VPP Custom", fallbackResults[5].DisplayName)
assert.NotNil(t, fallbackResults[5].VPPAppTeamID)
assert.Less(t, fallbackResults[4].ID, fallbackResults[5].ID)
}
type setupExperienceInsertTestRows struct {
HostUUID string `db:"host_uuid"`
Name string `db:"name"`
@ -1242,7 +1508,7 @@ func testSetupExperienceStatusResults(t *testing.T, ds *Datastore) {
insertSetupExperienceStatusResult(r)
}
res, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
res, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, 0)
require.NoError(t, err)
require.Len(t, res, 3)
for i, s := range expRes {
@ -1439,14 +1705,14 @@ func testUpdateSetupExperienceScriptWhileEnqueued(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.True(t, anythingEnqueued)
host1OriginalItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID)
host1OriginalItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID, team1.ID)
require.NoError(t, err)
require.Len(t, host1OriginalItems, 1)
require.Equal(t, fleet.SetupExperienceStatusPending, host1OriginalItems[0].Status)
require.NotNil(t, host1OriginalItems[0].SetupExperienceScriptID)
require.Equal(t, team1OriginalScript.ID, *host1OriginalItems[0].SetupExperienceScriptID)
host2OriginalItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam2UUID)
host2OriginalItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam2UUID, team2.ID)
require.NoError(t, err)
require.Len(t, host2OriginalItems, 1)
require.Equal(t, fleet.SetupExperienceStatusPending, host2OriginalItems[0].Status)
@ -1463,13 +1729,13 @@ func testUpdateSetupExperienceScriptWhileEnqueued(t *testing.T, ds *Datastore) {
require.Equal(t, team1OriginalScript.ScriptContentID, team1UpdatedScript.ScriptContentID)
require.Equal(t, team1OriginalScript.ID, team1UpdatedScript.ID)
host1NewItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID)
host1NewItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID, team1.ID)
require.NoError(t, err)
require.Len(t, host1NewItems, 1)
require.Equal(t, team1OriginalScript.ID, *host1NewItems[0].SetupExperienceScriptID)
// Should not have perturbed Host 2's enqueued execution either
host2NewItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam2UUID)
host2NewItems, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam2UUID, team2.ID)
require.NoError(t, err)
require.Len(t, host2NewItems, 1)
require.Equal(t, team2OriginalScript.ID, *host2NewItems[0].SetupExperienceScriptID)
@ -1484,12 +1750,12 @@ func testUpdateSetupExperienceScriptWhileEnqueued(t *testing.T, ds *Datastore) {
require.NotEqual(t, team1OriginalScript.ScriptContentID, team1UpdatedScript.ScriptContentID)
require.NotEqual(t, team1OriginalScript.ID, team1UpdatedScript.ID)
host1NewItems, err = ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID)
host1NewItems, err = ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID, team1.ID)
require.NoError(t, err)
require.Len(t, host1NewItems, 0)
// Should not have affected host 2's enqueued execution
host2NewItems, err = ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam2UUID)
host2NewItems, err = ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam2UUID, team2.ID)
require.NoError(t, err)
require.Len(t, host2NewItems, 1)
require.Equal(t, team2OriginalScript.ID, *host2NewItems[0].SetupExperienceScriptID)
@ -1499,7 +1765,7 @@ func testUpdateSetupExperienceScriptWhileEnqueued(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.True(t, anythingEnqueued)
host1NewItems, err = ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID)
host1NewItems, err = ds.ListSetupExperienceResultsByHostUUID(ctx, hostTeam1UUID, team1.ID)
require.NoError(t, err)
require.Len(t, host1NewItems, 1)
require.Equal(t, team1UpdatedScript.ID, *host1NewItems[0].SetupExperienceScriptID)

View file

@ -1750,7 +1750,7 @@ func testBatchSetSoftwareInstallersSetupExperienceSideEffects(t *testing.T, ds *
_, err = ds.EnqueueSetupExperienceItems(ctx, "darwin", "darwin", host1.UUID, *host1.TeamID)
require.NoError(t, err)
statuses, err := ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
statuses, err := ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID, team.ID)
require.NoError(t, err)
require.Len(t, statuses, 2)
@ -1800,7 +1800,7 @@ func testBatchSetSoftwareInstallersSetupExperienceSideEffects(t *testing.T, ds *
})
require.NoError(t, err)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID, team.ID)
require.NoError(t, err)
require.Len(t, statuses, 2)
@ -1845,7 +1845,7 @@ func testBatchSetSoftwareInstallersSetupExperienceSideEffects(t *testing.T, ds *
require.NoError(t, err)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID, team.ID)
require.NoError(t, err)
require.Len(t, statuses, 2)
@ -1917,7 +1917,7 @@ func testBatchSetSoftwareInstallersSetupExperienceSideEffects(t *testing.T, ds *
})
require.NoError(t, err)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID)
statuses, err = ds.ListSetupExperienceResultsByHostUUID(ctx, host1.UUID, team.ID)
require.NoError(t, err)
require.Len(t, statuses, 2)

View file

@ -2405,7 +2405,7 @@ type Datastore interface {
TeamIDsWithSetupExperienceIdPEnabled(ctx context.Context) ([]uint, error)
// ListSetupExperienceResultsByHostUUID lists the setup experience results for a host by its UUID.
ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string) ([]*SetupExperienceStatusResult, error)
ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string, teamID uint) ([]*SetupExperienceStatusResult, error)
// UpdateSetupExperienceStatusResult updates the given setup experience status result.
UpdateSetupExperienceStatusResult(ctx context.Context, status *SetupExperienceStatusResult) error

View file

@ -1531,7 +1531,7 @@ type GetTeamsWithInstallerByHashFunc func(ctx context.Context, sha256 string, ur
type TeamIDsWithSetupExperienceIdPEnabledFunc func(ctx context.Context) ([]uint, error)
type ListSetupExperienceResultsByHostUUIDFunc func(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error)
type ListSetupExperienceResultsByHostUUIDFunc func(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error)
type UpdateSetupExperienceStatusResultFunc func(ctx context.Context, status *fleet.SetupExperienceStatusResult) error
@ -9885,11 +9885,11 @@ func (s *DataStore) TeamIDsWithSetupExperienceIdPEnabled(ctx context.Context) ([
return s.TeamIDsWithSetupExperienceIdPEnabledFunc(ctx)
}
func (s *DataStore) ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) {
func (s *DataStore) ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) {
s.mu.Lock()
s.ListSetupExperienceResultsByHostUUIDFuncInvoked = true
s.mu.Unlock()
return s.ListSetupExperienceResultsByHostUUIDFunc(ctx, hostUUID)
return s.ListSetupExperienceResultsByHostUUIDFunc(ctx, hostUUID, teamID)
}
func (s *DataStore) UpdateSetupExperienceStatusResult(ctx context.Context, status *fleet.SetupExperienceStatusResult) error {

View file

@ -22896,7 +22896,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
// so pull it out manually
ubuntuHostUUID, err := fleet.HostUUIDForSetupExperience(ubuntuHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -22905,10 +22905,10 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["vim"])
require.Empty(t, executionIDs["vim"]) // hasn't run yet due to alphanumeric ordering
require.NotEmpty(t, executionIDs["test.tar.gz"])
// Record a result for vim.
// Record a result for test.tar.gz.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
"orbit_node_key": %q,
@ -22937,6 +22937,16 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
require.Equal(t, "vim", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "running", getDeviceStatusResponse.Results.Software[1].Status)
// get execution ID for vim
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
// Record a result for vim
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
@ -23021,7 +23031,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
// so pull it out manually
fedoraHostUUID, err := fleet.HostUUIDForSetupExperience(fedoraHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, fedoraHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, fedoraHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -23031,9 +23041,9 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
}
}
require.NotEmpty(t, executionIDs["ruby"])
require.NotEmpty(t, executionIDs["test.tar.gz"])
require.Empty(t, executionIDs["test.tar.gz"]) // hasn't run yet due to alphanumeric ordering
// Record a result for vim.
// Record a result for ruby.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
"orbit_node_key": %q,
@ -23062,6 +23072,16 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
require.Equal(t, "test.tar.gz", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "running", getDeviceStatusResponse.Results.Software[1].Status)
// get exec ID for test.tar.gz
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, fedoraHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
// Record a result for test.tar.gz.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
@ -23130,7 +23150,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
// so pull it out manually
ubuntuHostUUID, err := fleet.HostUUIDForSetupExperience(ubuntuHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -23139,7 +23159,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["vim"])
require.Empty(t, executionIDs["vim"]) // hasn't run yet due to alphanumeric ordering
require.NotEmpty(t, executionIDs["test.tar.gz"])
// Cancel the software install for test.tar.gz.
@ -23158,9 +23178,19 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
return getDeviceStatusResponse.Results.Software[i].Name < getDeviceStatusResponse.Results.Software[j].Name
})
require.Equal(t, "test.tar.gz", getDeviceStatusResponse.Results.Software[0].Name)
require.EqualValues(t, "failure", getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, fleet.SetupExperienceStatusFailure, getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, "vim", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "running", getDeviceStatusResponse.Results.Software[1].Status)
require.Equal(t, fleet.SetupExperienceStatusPending, getDeviceStatusResponse.Results.Software[1].Status)
// Get execution ID for vim
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
// Record a result for vim.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
@ -23187,9 +23217,9 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
return getDeviceStatusResponse.Results.Software[i].Name < getDeviceStatusResponse.Results.Software[j].Name
})
require.Equal(t, "test.tar.gz", getDeviceStatusResponse.Results.Software[0].Name)
require.EqualValues(t, "failure", getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, fleet.SetupExperienceStatusFailure, getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, "vim", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "success", getDeviceStatusResponse.Results.Software[1].Status)
require.Equal(t, fleet.SetupExperienceStatusSuccess, getDeviceStatusResponse.Results.Software[1].Status)
})
t.Run("ubuntu-software-edited-during-se", func(t *testing.T) {
@ -23224,7 +23254,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
// so pull it out manually
ubuntuHostUUID, err := fleet.HostUUIDForSetupExperience(ubuntuHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -23233,13 +23263,39 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["vim"])
require.Empty(t, executionIDs["vim"]) // hasn't run yet due to alphanumeric ordering
require.NotEmpty(t, executionIDs["test.tar.gz"])
// Record a result for test.tar.gz.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
"orbit_node_key": %q,
"install_uuid": %q,
"install_script_exit_code": 0,
"install_script_output": "ok"
}`,
*ubuntuHost.OrbitNodeKey,
executionIDs["test.tar.gz"],
),
), http.StatusNoContent)
// Get status of the "Setup experience" for the Ubuntu host.
getDeviceStatusResponse = getDeviceSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/v1/fleet/device/fleet-desktop-token-ubuntu/setup_experience/status",
getDeviceSetupExperienceStatusRequest{}, http.StatusOK, &getDeviceStatusResponse,
)
require.NoError(t, getDeviceStatusResponse.Err)
require.NotNil(t, getDeviceStatusResponse.Results)
require.Len(t, getDeviceStatusResponse.Results.Software, 2)
require.Equal(t, "test.tar.gz", getDeviceStatusResponse.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, "vim", getDeviceStatusResponse.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusRunning, getDeviceStatusResponse.Results.Software[1].Status)
// Modify the vim installer, which should cause the setup experience item to fail.
// update should succeed
s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
SelfService: ptr.Bool(true),
SelfService: new(true),
InstallScript: ptr.String("some updated install script"),
PreInstallQuery: ptr.String("some new pre install query"),
PostInstallScript: ptr.String("some new post install script"),
@ -23248,7 +23304,6 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
TeamID: &team.ID,
}, http.StatusOK, "")
// Get status of the "Setup experience" for the Ubuntu host.
getDeviceStatusResponse = getDeviceSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/v1/fleet/device/fleet-desktop-token-ubuntu/setup_experience/status",
getDeviceSetupExperienceStatusRequest{}, http.StatusOK, &getDeviceStatusResponse,
@ -23260,9 +23315,21 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
return getDeviceStatusResponse.Results.Software[i].Name < getDeviceStatusResponse.Results.Software[j].Name
})
require.Equal(t, "test.tar.gz", getDeviceStatusResponse.Results.Software[0].Name)
require.EqualValues(t, "running", getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, fleet.SetupExperienceStatusSuccess, getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, "vim", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "failure", getDeviceStatusResponse.Results.Software[1].Status)
require.Equal(t, fleet.SetupExperienceStatusFailure, getDeviceStatusResponse.Results.Software[1].Status)
// Get execution ID for vim
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["vim"])
require.NotEmpty(t, executionIDs["test.tar.gz"])
s.lastActivityOfTypeMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), fmt.Sprintf(`{
"host_id": %d,
@ -23303,9 +23370,9 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
return getDeviceStatusResponse.Results.Software[i].Name < getDeviceStatusResponse.Results.Software[j].Name
})
require.Equal(t, "test.tar.gz", getDeviceStatusResponse.Results.Software[0].Name)
require.EqualValues(t, "success", getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, fleet.SetupExperienceStatusSuccess, getDeviceStatusResponse.Results.Software[0].Status)
require.Equal(t, "vim", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "failure", getDeviceStatusResponse.Results.Software[1].Status)
require.Equal(t, fleet.SetupExperienceStatusFailure, getDeviceStatusResponse.Results.Software[1].Status)
})
// Transfer the Ubuntu host to "No team".
@ -23378,7 +23445,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware()
// so pull it out manually
ubuntuHostUUID, err := fleet.HostUUIDForSetupExperience(ubuntuHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 1)
require.NotNil(t, results[0].HostSoftwareInstallsExecutionID)
@ -23512,7 +23579,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftwareWit
// so pull it out manually
ubuntuHostUUID, err := fleet.HostUUIDForSetupExperience(ubuntuHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -23521,10 +23588,10 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftwareWit
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["vim"])
require.Empty(t, executionIDs["vim"]) // hasn't run yet due to alphanumeric ordering
require.NotEmpty(t, executionIDs["test.tar.gz"])
// Record a result for vim.
// Record a result for test.tar.gz.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
"orbit_node_key": %q,
@ -23553,6 +23620,16 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftwareWit
require.Equal(t, "vim", orbitRes.Results.Software[1].Name)
require.EqualValues(t, "running", orbitRes.Results.Software[1].Status)
// Get execution ID for vim
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
// Record a result for vim.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
@ -23756,7 +23833,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftware(
// so pull it out manually
windowsHostUUID, err := fleet.HostUUIDForSetupExperience(windowsHost1)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -23766,7 +23843,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftware(
}
}
require.NotEmpty(t, executionIDs["Fleet osquery"])
require.NotEmpty(t, executionIDs["Hello world"])
require.Empty(t, executionIDs["Hello world"]) // hasn't run yet due to alphanumeric ordering
// Record a result for Fleet osquery.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
@ -23797,6 +23874,18 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftware(
require.Equal(t, "Hello world", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "running", getDeviceStatusResponse.Results.Software[1].Status)
// Get execution ID for Hello world
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["Fleet osquery"])
require.NotEmpty(t, executionIDs["Hello world"])
// Record a result for Hello world.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
@ -23975,7 +24064,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftware(
// so pull it out manually
windowsHostUUID, err := fleet.HostUUIDForSetupExperience(windowsHost2)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -23985,7 +24074,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftware(
}
}
require.NotEmpty(t, executionIDs["Fleet osquery"])
require.NotEmpty(t, executionIDs["Hello world"])
require.Empty(t, executionIDs["Hello world"]) // hasn't run yet due to alphanumeric ordering
// Record a failing result for Fleet osquery.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
@ -24016,6 +24105,18 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftware(
require.Equal(t, "Hello world", getDeviceStatusResponse.Results.Software[1].Name)
require.EqualValues(t, "running", getDeviceStatusResponse.Results.Software[1].Status)
// Get execution ID for Hello world
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["Fleet osquery"])
require.NotEmpty(t, executionIDs["Hello world"])
// Record a result for Hello world.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
fmt.Sprintf(`{
@ -24176,9 +24277,9 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftwareW
// The setup_experience/status endpoint doesn't return the various IDs for executions,
// so pull it out manually
ubuntuHostUUID, err := fleet.HostUUIDForSetupExperience(windowsHost)
windowsHostUUID, err := fleet.HostUUIDForSetupExperience(windowsHost)
require.NoError(t, err)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, ubuntuHostUUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
executionIDs := make(map[string]string) // installer name -> install execution ID
@ -24187,8 +24288,9 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftwareW
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.NotEmpty(t, executionIDs["Fleet osquery"])
require.NotEmpty(t, executionIDs["Hello world"])
require.NotEmpty(t, executionIDs["Fleet osquery"]) // has started running because it's first alphabetically
require.Empty(t, executionIDs["Hello world"]) // not running yet
// Record a result for Fleet osquery.
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(
@ -24203,7 +24305,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftwareW
),
), http.StatusNoContent)
// Again get status of the "Setup experience" for the Windos host.
// Again get status of the "Setup experience" for the Windows host.
orbitRes = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status",
fleet.GetOrbitSetupExperienceStatusRequest{OrbitNodeKey: *windowsHost.OrbitNodeKey}, http.StatusOK, &orbitRes,
@ -24211,9 +24313,16 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftwareW
require.NotNil(t, orbitRes.Results)
require.NotNil(t, orbitRes.Results)
require.Len(t, orbitRes.Results.Software, 2)
sort.Slice(orbitRes.Results.Software, func(i, j int) bool {
return orbitRes.Results.Software[i].Name < orbitRes.Results.Software[j].Name
})
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, windowsHostUUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 2)
for _, result := range results {
if result.HostSoftwareInstallsExecutionID != nil {
executionIDs[result.Name] = *result.HostSoftwareInstallsExecutionID
}
}
require.Equal(t, "Fleet osquery", orbitRes.Results.Software[0].Name)
require.EqualValues(t, "success", orbitRes.Results.Software[0].Status)
require.Equal(t, "Hello world", orbitRes.Results.Software[1].Name)
@ -24232,7 +24341,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceWindowsWithSoftwareW
),
), http.StatusNoContent)
// One last time get status of the "Setup experience" for the Ubuntu host.
// One last time get status of the "Setup experience" for the Windows host.
orbitRes = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status",
fleet.GetOrbitSetupExperienceStatusRequest{OrbitNodeKey: *windowsHost.OrbitNodeKey}, http.StatusOK, &orbitRes,

View file

@ -8,6 +8,7 @@ import (
"io"
"net/http"
"os"
"slices"
"sort"
"strings"
"testing"
@ -365,7 +366,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// it out manually
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, *enrolledHost.TeamID)
require.NoError(t, err)
require.Len(t, results, 2)
var installUUID string
@ -471,7 +472,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Script.Status)
// Get script exec ID
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, *enrolledHost.TeamID)
require.NoError(t, err)
require.Len(t, results, 2)
var execID string
@ -836,7 +837,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithFMAAndVersionRollba
require.Equal(t, fmaTitleID, *fmaResult.SoftwareTitleID)
// Pull the execution ID out of the DB (the status endpoint doesn't surface it).
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, tm.ID)
require.NoError(t, err)
require.Len(t, results, 1)
require.NotNil(t, results[0].HostSoftwareInstallsExecutionID)
@ -1235,29 +1236,53 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
}
require.ElementsMatch(t, []string{"N1", "Fleetd configuration", "Fleet root certificate authority (CA)"}, profNames)
require.ElementsMatch(t, []fleet.MDMDeliveryStatus{fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerifying, fleet.MDMDeliveryVerifying}, profStatuses)
// the software and script are still pending
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 2)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, "App 5", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
require.Equal(t, "App 5", statusResp.Results.Software[1].Name)
require.Equal(t, "DummyApp", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[1].Status)
// Get status: the VPP app install should have run and failed.
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved
require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved
require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile
require.Len(t, statusResp.Results.Software, 2)
// App 5 has no licenses available, so we should get a status failed here...
require.Equal(t, "App 5", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[0].Status)
// ...but setup experience should still continue with the next app in the list
require.Equal(t, "DummyApp", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[1].Status)
// Script goes last
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
// The status for DummyApp should be "running" now, since it's started installing
// but we haven't sent back an installation status from orbit yet
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Len(t, statusResp.Results.Software, 2)
require.Equal(t, "DummyApp", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Software[1].Status)
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// it out manually
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
// exec ID for "DummyApp" out manually
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 3)
var installUUID string
for _, r := range results {
if r.HostSoftwareInstallsExecutionID != nil &&
r.SoftwareInstallerID != nil &&
r.Name == statusResp.Results.Software[0].Name {
r.Name == statusResp.Results.Software[1].Name {
installUUID = *r.HostSoftwareInstallsExecutionID
break
}
@ -1267,7 +1292,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
// Need to get the software title to get the package name
var getSoftwareTitleResp getSoftwareTitleResponse
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *statusResp.Results.Software[0].SoftwareTitleID), nil, http.StatusOK, &getSoftwareTitleResp, "team_id", fmt.Sprintf("%d", *enrolledHost.TeamID))
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *statusResp.Results.Software[1].SoftwareTitleID), nil, http.StatusOK, &getSoftwareTitleResp, "team_id", fmt.Sprintf("%d", *enrolledHost.TeamID))
require.NotNil(t, getSoftwareTitleResp.SoftwareTitle)
require.NotNil(t, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage)
@ -1280,37 +1305,21 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
"install_script_output": "ok"
}`, *enrolledHost.OrbitNodeKey, installUUID)), http.StatusNoContent)
// Script is already running, poll again to confirm status
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Nil(t, statusResp.Results.BootstrapPackage) // no bootstrap package involved
require.Nil(t, statusResp.Results.AccountConfiguration) // no SSO involved
require.Len(t, statusResp.Results.ConfigurationProfiles, 3) // fleetd config, root CA, custom profile
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 2)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
// App 5 has no licenses available, so we should get a status failed here and setup experience
// should continue
require.Equal(t, "App 5", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[1].Status)
// Software installations are done, now we should run the script
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.Equal(t, "App 5", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Script.Status)
require.Equal(t, "App 5", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[1].Status)
require.Equal(t, "DummyApp", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[1].Status)
// Get script exec ID
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 3)
var execID string
@ -1330,12 +1339,12 @@ func (s *integrationMDMTestSuite) TestSetupExperienceVPPInstallError() {
// release of the device, as all setup experience steps are now complete.
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
require.Equal(t, "App 5", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
require.Equal(t, "App 5", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[1].Status)
require.Equal(t, "DummyApp", statusResp.Results.Software[1].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[1].Status)
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
@ -1436,7 +1445,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowUpdateScript() {
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// it out manually (for now only the software install has its execution id)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID, tm.ID)
require.NoError(t, err)
require.Len(t, results, 2)
@ -1527,7 +1536,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowUpdateScript() {
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// them out manually
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID, tm.ID)
require.NoError(t, err)
require.Len(t, results, 1)
var installUUIDs []string
@ -1634,7 +1643,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowCancelScript() {
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// it out manually (for now only the software install has its execution id)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID, *host.TeamID)
require.NoError(t, err)
require.Len(t, results, 2)
@ -1681,7 +1690,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowCancelScript() {
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// it out manually (this time get the script exec ID)
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID)
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, host.UUID, *host.TeamID)
require.NoError(t, err)
require.Len(t, results, 2)
@ -1878,6 +1887,10 @@ func (s *integrationMDMTestSuite) TestSetupExperienceWithLotsOfVPPApps() {
expectedApps := []*fleet.VPPApp{macOSApp1, macOSApp2, macOSApp3, macOSApp4, macOSApp5, macOSApp6}
slices.SortFunc(expectedApps, func(a, b *fleet.VPPApp) int {
return strings.Compare(a.Name, b.Name)
})
expectedAppsByName := map[string]*fleet.VPPApp{
macOSApp1.Name: macOSApp1,
macOSApp2.Name: macOSApp2,
@ -2144,18 +2157,18 @@ func (s *integrationMDMTestSuite) TestSetupExperienceWithLotsOfVPPApps() {
if software.Name == macOSApp1.Name {
require.Equal(t, fleet.SetupExperienceStatusSuccess, software.Status)
} else {
require.Equal(t, fleet.SetupExperienceStatusRunning, software.Status)
require.Equal(t, fleet.SetupExperienceStatusPending, software.Status)
}
require.NotNil(t, software.SoftwareTitleID)
require.NotZero(t, *software.SoftwareTitleID)
}
// All apps should have an install record at this point
// Only 2 apps have an installation attempt at this point
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
var count int
err := sqlx.GetContext(context.Background(), q, &count, "SELECT COUNT(*) FROM host_vpp_software_installs")
require.NoError(t, err)
require.Equal(t, 6, count)
require.Equal(t, 2, count)
return nil
})
@ -2163,7 +2176,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceWithLotsOfVPPApps() {
macOSApp1.Name: {},
}
for _, app := range expectedApps {
for _, app := range expectedApps[1:] {
opts.softwareResultList = append(opts.softwareResultList, fleet.Software{
Name: app.Name,
BundleIdentifier: app.BundleIdentifier,
@ -2186,9 +2199,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceWithLotsOfVPPApps() {
require.True(t, ok)
_, shouldBeInstalled := installedApps[software.Name]
if shouldBeInstalled {
require.Equal(t, fleet.SetupExperienceStatusSuccess, software.Status, software.Name, software.Status)
} else {
require.Equal(t, fleet.SetupExperienceStatusRunning, software.Status)
require.Equalf(t, fleet.SetupExperienceStatusSuccess, software.Status, "software %s should have succeeded", software.Name)
}
require.NotNil(t, software.SoftwareTitleID)
require.NotZero(t, *software.SoftwareTitleID)
@ -3269,7 +3280,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequireSoftware() {
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// them out manually
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, *enrolledHost.TeamID)
require.NoError(t, err)
require.Len(t, results, 4)
var installUUIDs []string
@ -3278,7 +3289,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequireSoftware() {
installUUIDs = append(installUUIDs, *r.HostSoftwareInstallsExecutionID)
}
}
require.Equal(t, len(installUUIDs), 3)
require.Len(t, installUUIDs, 1)
// debugPrintActivities := func(activities []*fleet.UpcomingActivity) []string {
// var res []string
@ -3320,10 +3331,14 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequireSoftware() {
// Since no results have been recorded, this shouldn't change any database state.
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
// Software is now running, script is still pending
// First software is now running, other software and script are still pending
require.Equal(t, len(statusResp.Results.Software), 3)
for _, softwareResult := range statusResp.Results.Software {
require.Equal(t, fleet.SetupExperienceStatusRunning, softwareResult.Status)
for i, softwareResult := range statusResp.Results.Software {
if i == 0 {
require.Equal(t, fleet.SetupExperienceStatusRunning, softwareResult.Status)
continue
}
require.Equal(t, fleet.SetupExperienceStatusPending, softwareResult.Status)
}
require.NotNil(t, statusResp.Results.Script)
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
@ -3338,6 +3353,17 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequireSoftware() {
"install_script_output": "ok"
}`, *enrolledHost.OrbitNodeKey, installUUIDs[0])), http.StatusNoContent)
results, err = s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, *enrolledHost.TeamID)
require.NoError(t, err)
require.Len(t, results, 4)
installUUIDs = []string{}
for _, r := range results {
if r.HostSoftwareInstallsExecutionID != nil {
installUUIDs = append(installUUIDs, *r.HostSoftwareInstallsExecutionID)
}
}
require.Len(t, installUUIDs, 2)
// status still shows script as pending
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
@ -3350,9 +3376,8 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequireSoftware() {
require.Len(t, statusResp.Results.Software, 3)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
// Other two software should still be "running"
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Software[1].Status)
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Software[2].Status)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[2].Status)
// Record a failure for the second software.
s.Do("POST", "/api/fleet/orbit/software_install/result",
@ -3458,9 +3483,28 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequiredSoftwareVPP
// Add the app with 1 licenses available
s.Do("POST", "/api/latest/fleet/software/app_store_apps", &addAppStoreAppRequest{TeamID: &team.ID, AppStoreID: "4", SelfService: true}, http.StatusOK)
// Add the VPP app to setup experience
// Set custom display names on the VPP apps so they sort alphabetically
// after DummyApp, with the no-license app sorting first among the VPP
// apps. This ensures the installer (DummyApp) installs first, then the
// VPP app with no license fails immediately, triggering the
// "require all software" cancel-on-failure flow after a successful
// installer install.
// Alphabetical order by display name: DummyApp < VPP AAA No License < VPP ZZZ Has License
//
// This also exercises the fix for #41741: setup experience ordering
// should use the custom display name when set.
vppTitleID := getSoftwareTitleID(t, s.ds, "App 5", "apps")
vppTitleID2 := getSoftwareTitleID(t, s.ds, "App 4", "apps")
var updateAppResp updateAppStoreAppResponse
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppTitleID),
&updateAppStoreAppRequest{TeamID: &team.ID, DisplayName: ptr.String("VPP ZZZ Has License")},
http.StatusOK, &updateAppResp)
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/software/titles/%d/app_store_app", vppTitleID2),
&updateAppStoreAppRequest{TeamID: &team.ID, DisplayName: ptr.String("VPP AAA No License")},
http.StatusOK, &updateAppResp)
// Add the VPP apps to setup experience
installerTitleID := getSoftwareTitleID(t, s.ds, "DummyApp", "apps")
var swInstallResp putSetupExperienceSoftwareResponse
s.DoJSON("PUT", "/api/v1/fleet/setup_experience/software", putSetupExperienceSoftwareRequest{TeamID: team.ID, TitleIDs: []uint{vppTitleID, vppTitleID2, installerTitleID}}, http.StatusOK, &swInstallResp)
@ -3562,18 +3606,30 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequiredSoftwareVPP
require.Equal(t, "script.sh", statusResp.Results.Script.Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status)
require.Len(t, statusResp.Results.Software, 3)
// Ordered by display name: DummyApp (no display name), App 4 (display name
// "VPP AAA No License"), App 5 (display name "VPP ZZZ Has License").
// Note: Name stores the original st.name, ordering uses display name.
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[0].Status)
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
require.Equal(t, "App 4", statusResp.Results.Software[1].Name)
require.Equal(t, "VPP AAA No License", statusResp.Results.Software[1].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[1].Status)
require.Equal(t, "App 5", statusResp.Results.Software[2].Name)
require.Equal(t, "VPP ZZZ Has License", statusResp.Results.Software[2].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[2].Status)
// call /status endpoint again, DummyApp (first by display name) should be running
statusResp = fleet.GetOrbitSetupExperienceStatusResponse{}
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)
require.Len(t, statusResp.Results.Software, 3)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name, "DummyApp should be first by display name")
require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Software[0].Status)
// The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull
// it out manually
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID)
results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID, team.ID)
require.NoError(t, err)
require.Len(t, results, 4)
var installUUID string
@ -3614,11 +3670,15 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequiredSoftwareVPP
require.Len(t, statusResp.Results.Software, 3)
require.Equal(t, "DummyApp", statusResp.Results.Software[0].Name)
require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status)
// App 4 has no licenses available, so it should fail and because we have "requre_all_software_macos" set,
// the other software and the script should be marked as failed too.
// App 4 (display name "VPP AAA No License") has no licenses available, so
// it should fail immediately. Because we have "require_all_software_macos"
// set, the remaining software (App 5) and the script should be marked as
// failed too.
require.Equal(t, "App 4", statusResp.Results.Software[1].Name)
require.Equal(t, "VPP AAA No License", statusResp.Results.Software[1].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[1].Status)
require.Equal(t, "App 5", statusResp.Results.Software[2].Name)
require.Equal(t, "VPP ZZZ Has License", statusResp.Results.Software[2].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusFailure, statusResp.Results.Software[2].Status)
// Reset the setup experience items.
@ -3634,8 +3694,10 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithRequiredSoftwareVPP
require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID)
require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID)
require.Equal(t, "App 4", statusResp.Results.Software[1].Name)
require.Equal(t, "VPP AAA No License", statusResp.Results.Software[1].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[1].Status)
require.Equal(t, "App 5", statusResp.Results.Software[2].Name)
require.Equal(t, "VPP ZZZ Has License", statusResp.Results.Software[2].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Software[2].Status)
}
@ -3872,7 +3934,9 @@ func (s *integrationMDMTestSuite) TestSetupExperienceMacOSCustomDisplayNameIcon(
require.NoError(t, res.Body.Close())
require.NoError(t, deviceResp.Err)
// the software is now running (because previous call to /status kickstarted the process), and script is pending
// Software is installed one at a time in alphabetical order by display
// name (falling back to name). The previous call to /status kickstarted
// the first item; the second remains pending until the first finishes.
require.Len(t, deviceResp.Results.Scripts, 1)
require.Equal(t, "script.sh", deviceResp.Results.Scripts[0].Name)
require.Equal(t, fleet.SetupExperienceStatusPending, deviceResp.Results.Scripts[0].Status)
@ -3885,7 +3949,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceMacOSCustomDisplayNameIcon(
require.Empty(t, deviceResp.Results.Software[0].IconURL)
require.Equal(t, "EchoApp", deviceResp.Results.Software[1].Name)
require.Equal(t, "My Custom EchoApp", deviceResp.Results.Software[1].DisplayName)
require.Equal(t, fleet.SetupExperienceStatusRunning, deviceResp.Results.Software[1].Status)
require.Equal(t, fleet.SetupExperienceStatusPending, deviceResp.Results.Software[1].Status)
require.NotNil(t, deviceResp.Results.Software[1].SoftwareTitleID)
require.Equal(t, echoTitleID, *deviceResp.Results.Software[1].SoftwareTitleID)
require.NotEmpty(t, deviceResp.Results.Software[1].IconURL)

View file

@ -894,7 +894,7 @@ func (svc *Service) hostIsInSetupExperience(ctx context.Context, host *fleet.Hos
if err != nil {
return false, ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
}
inSetupExperience, err := svc.hasSetupExperiencePendingOrRunningItems(ctx, hostUUID)
inSetupExperience, err := svc.hasSetupExperiencePendingOrRunningItems(ctx, hostUUID, ptr.ValOrZero(host.TeamID))
if err != nil && !fleet.IsNotFound(err) {
return false, ctxerr.Wrap(ctx, err, "check setup experience pending or running items")
}
@ -904,8 +904,8 @@ func (svc *Service) hostIsInSetupExperience(ctx context.Context, host *fleet.Hos
}
}
func (svc *Service) hasSetupExperiencePendingOrRunningItems(ctx context.Context, hostUUID string) (bool, error) {
statuses, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
func (svc *Service) hasSetupExperiencePendingOrRunningItems(ctx context.Context, hostUUID string, teamID uint) (bool, error) {
statuses, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, teamID)
if err != nil {
return false, ctxerr.Wrap(ctx, err, "retrieving setup experience results")
}

View file

@ -1705,7 +1705,7 @@ func TestDetailQueriesWithEmptyStrings(t *testing.T) {
}
return host, nil
}
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) {
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) {
return nil, nil
}
@ -1943,7 +1943,7 @@ func TestDetailQueries(t *testing.T) {
ds.GetHostAwaitingConfigurationFunc = func(ctx context.Context, hostuuid string) (bool, error) {
return false, nil
}
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) {
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) {
return nil, nil
}
@ -2476,7 +2476,7 @@ func TestDistributedQueryResults(t *testing.T) {
EnableSoftwareInventory: true,
}}, nil
}
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string) ([]*fleet.SetupExperienceStatusResult, error) {
ds.ListSetupExperienceResultsByHostUUIDFunc = func(ctx context.Context, hostUUID string, teamID uint) ([]*fleet.SetupExperienceStatusResult, error) {
return nil, nil
}

View file

@ -277,7 +277,7 @@ func maybeCancelPendingSetupExperienceSteps(ctx context.Context, ds fleet.Datast
if err != nil {
return ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience")
}
statuses, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
statuses, err := ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, ptr.ValOrZero(host.TeamID))
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step")
}

View file

@ -127,7 +127,7 @@ func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArg
// We shouldn't have any setup experience steps if we're not on a premium license,
// but best to check anyway plus it saves some db queries.
if license.IsPremium(ctx) {
_, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID)
_, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID, ptr.ValOrZero(args.TeamID))
if err != nil {
return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS")
}
@ -189,7 +189,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
}
}
} else {
commandUUIDs, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID)
commandUUIDs, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID, ptr.ValOrZero(args.TeamID))
if err != nil {
return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS")
}
@ -482,7 +482,7 @@ func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArg
}
if !isMacOS(args.Platform) {
setupExperienceStatuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, args.HostUUID)
setupExperienceStatuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, args.HostUUID, ptr.ValOrZero(args.TeamID))
if err != nil {
return ctxerr.Wrap(ctx, err, "retrieving setup experience status results for host pending DEP release")
}
@ -523,8 +523,8 @@ func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) (string,
return cmdUUID, nil
}
func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context, hostUUID string) ([]string, error) {
statuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context, hostUUID string, teamID uint) ([]string, error) {
statuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, hostUUID, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step")
}
@ -583,9 +583,6 @@ func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context,
cmdUUID, err := a.installSoftwareFromVPP(ctx, host, vppApp, true, opts)
app.NanoCommandUUID = &cmdUUID
app.Status = fleet.SetupExperienceStatusRunning
if err != nil {
// if we get an error (e.g. no available licenses) while attempting to enqueue the
// install, then we should immediately go to an error state so setup experience
@ -594,6 +591,8 @@ func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context,
app.Status = fleet.SetupExperienceStatusFailure
app.Error = ptr.String(err.Error())
} else {
app.NanoCommandUUID = &cmdUUID
app.Status = fleet.SetupExperienceStatusRunning
commandUUIDs = append(commandUUIDs, cmdUUID)
}
if err := a.Datastore.UpdateSetupExperienceStatusResult(ctx, app); err != nil {

View file

@ -1108,7 +1108,7 @@ INSERT INTO setup_experience_status_results (
}
require.ElementsMatch(t, expectedAdamIDs, installedAdamIDs)
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID)
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID, tm.ID)
require.NoError(t, err)
require.Len(t, results, len(expectedAppInstalls))
for _, result := range results {
@ -1279,7 +1279,7 @@ INSERT INTO setup_experience_status_results (
}
require.ElementsMatch(t, expectedAdamIDs, installedAdamIDs)
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID)
results, err := ds.ListSetupExperienceResultsByHostUUID(ctx, h.UUID, tm.ID)
require.NoError(t, err)
require.Len(t, results, len(expectedAppInstalls))
for _, result := range results {