Skip to content

Setup Experience IPAs: Enqueueing and install during setup, command results handler #50629

Description

@JordanMontgomery

Related user story

#33995

Task

Make a selected .ipa actually install while the device is held in Setup Assistant, and make its result drive the device release.

Types

server/fleet/setup_experience.go:

  • Add InHouseAppID *uint \db:"in_house_app_id" json:"-"`toSetupExperienceStatusResult (:37-70`).
  • Extend IsValid() (:72-105) to count it as a fourth mutually exclusive column, and to reject it paired with HostSoftwareInstallsExecutionID or ScriptExecutionID (it pairs with NanoCommandUUID, like VPP). Keep the existing policy_gated guard installer-only.
  • Include it in IsForSoftware() (:122-128) and add IsForInHouseApp().

Enqueue

enqueueSetupExperienceItems (server/datastore/mysql/setup_experience.go:31) — add a third UNION branch, gated on fleetPlatform being ios/ipados, modelled on the VPP branch at :298-331:

SELECT ? AS host_uuid, st.name AS name, 'pending' AS status,
    NULL AS software_installer_id, NULL AS vpp_app_team_id, iha.id AS in_house_app_id,
    FALSE AS policy_gated,
    COALESCE(stdn.display_name, st.name) AS sort_name, st.id AS software_title_id
FROM in_house_apps iha
INNER JOIN software_titles st ON iha.title_id = st.id
LEFT JOIN software_title_display_names stdn ON stdn.software_title_id = st.id AND stdn.team_id = ?
WHERE iha.install_during_setup = true AND iha.global_or_team_id = ? AND iha.platform = ?

Add in_house_app_id to the INSERT column list and the inner SELECT at :338-356. The ROW_NUMBER() OVER (PARTITION BY software_title_id ...) dedup is a no-op across the iOS and iPadOS rows (different titles), but the partition list still needs the new column carried through. Mirror the branch in the resetFailedSetupSteps variant.

ListSetupExperienceResultsByHostUUID (:742) — LEFT JOIN in_house_apps iha ON iha.id = sesr.in_house_app_id and extend the software_title_id and source COALESCE chains (:759-763) so display name and icon resolution work for in-house rows.

Install during Setup Assistant

installSetupExperienceVPPAppsOnIosIpadOS (server/worker/apple_mdm.go:575) is the whole iOS/iPadOS setup-experience driver — called from post-DEP enrollment (:204-208) and post-manual enrollment (:136-141). Today it fails any non-VPP row at :593-601 with "unexpected setup experience item for iOS/iPadOS device, only VPP apps are supported". Rename it (installSetupExperienceAppsOnIosIpadOS) and add an in-house branch to the loop at :604-667:

  • Enqueue via the equivalent of InsertHostInHouseAppInstall (server/datastore/mysql/in_house_apps.go:531) with fleet.HostSoftwareInstallOptions{SelfService: false, ForSetupExperience: true}.
  • The enqueue must carry the precheckAppConfigResolvable pre-flight from the manual install path (ee/server/service/software_installers.go:1665-1670), so an unresolvable $FLEET_VAR_* fails the item with a user-facing reason instead of producing a broken command. That path is in ee, so expose it to the worker the same way VPP is — an installer interface on the worker mirroring VPPInstaller (server/worker/apple_mdm.go:672-683) — rather than duplicating the logic.
  • Do not carry over the label-scope check. The manual path calls IsInHouseAppLabelScoped and hard-fails an out-of-scope host (ee/server/service/software_installers.go:1649-1658); setup experience must skip that call entirely. Reuse the config pre-flight without it.
  • On success, set NanoCommandUUID to the command UUID (InsertHostInHouseAppInstall's commandUUID is the upcoming_activities.execution_id and becomes the MDM command UUID in activateNextInHouseAppInstallActivity), set status running, and append to the returned slice so the UUID lands in awaitCmdUUIDs for the release job.
  • On failure before the command is sent, set status failure + Error, and emit a failure activity — mirroring :636-665.

Why no label check (product decision): setup experience ignores label scoping for every software type today. VPP's setup-experience path at :633 has no scope check — IsVPPAppLabelScoped only guards manual and self-service installs — and enqueueSetupExperienceItems states it outright in the comment above its INSERT: "labels don't apply during setup" (server/datastore/mysql/setup_experience.go:335-337). Enforcing them for .ipa alone would also be unreliable: a freshly ABM-enrolled iOS host has little to no computed label membership at post-DEP time, so an out-of-scope result would usually be a race rather than intent. The enqueue query in the previous section deliberately has no in_house_app_labels join.

SetupExperienceNextStep (ee/server/service/setup_experience.go:324-415) switches on SoftwareInstallerID then VPPAppTeamID. iOS never reaches it, but an unhandled in-house row would fall through both cases and silently stall the flow. Add a branch or an explicit guard.

Command results

  • In-house install failure produces no activity today. The InstallApplication error branch (server/service/apple_mdm.go:5107-5158) calls GetHostVPPInstallByCommandUUID (nil for in-house, so no retry — correct), then maybeUpdateSetupExperienceStatus (works for in-house, since it keys on nano_command_uuid), then GetPastActivityDataForVPPAppInstall, which returns not-found and return nil, nil at :5147-5150. Add a GetPastActivityDataForInHouseAppInstall fallback (server/datastore/mysql/in_house_apps.go:683) before bailing.
  • Tag the setup-experience activity. The in-house activityFn in server/service/apple_mdm_cmd_results.go:224 drops the fromSetupExp argument. Plumb it into ActivityTypeInstalledSoftware.FromSetupExperience (server/fleet/activities.go:1211), which already exists.

No new terminal-status datastore method. MaybeUpdateSetupExperienceVPPStatus (server/datastore/mysql/setup_experience.go:1098) matches host_uuid + nano_command_uuid and already fires for in-house installs from inside the shared closure at apple_mdm_cmd_results.go:169. Consider renaming it to drop "VPP" since it now serves both.

Run make generate-mock after any datastore interface change.

Condition of satisfaction

Enqueue:

  • An ABM-enrolled iPhone on a team with the iOS title selected gets exactly one setup_experience_status_results row with in_house_app_id set and awaiting_configuration = 1.
  • An enrolling iPad on the same team gets nothing when only the iOS title is selected; the inverse holds when the selection is swapped.
  • IsValid() rejects a row with both in_house_app_id and vpp_app_team_id, and a row with in_house_app_id + host_software_installs_execution_id.
  • Tests pass for manual enrollment as well

Install and release:

  • Fleet enqueues an InstallApplication with ManifestURL pointing at /api/latest/fleet/software/titles/{id}/in_house_app/manifest/{token}; the token TTL (fleet.InHouseAppInstallTokenTTL, 6h) comfortably covers Setup Assistant.
  • DeviceConfigured is not sent until the row reaches a terminal status; verified against the setup_experience_status_results gate at server/worker/apple_mdm.go:533-555.
  • Post-Setup-Assistant, the app is installed and launchable; host software inventory shows it as Installed and the install activity appears on Host details.
  • Mixed payload: a .ipa and a VPP app on the same platform both install, ordered by the sort_name ordering applied at enqueue.

Failure paths:

  • A failing install (incompatible OS version, or a corrupt/unsigned manifest) flips the row to failure, emits an installed_software activity with status: failed and from_setup_experience: true, and the host is still released — not left in AwaitingConfiguration.
  • An install that never reaches a terminal state is released by the existing cap (15 min / 30 attempts).
  • Confirm no cancel cascade fires: isAllSetupExperienceSoftwareRequired (server/service/setup_experience.go:242) returns false for iOS, so remaining items are not canceled.

Label scoping ignored:

  • A title with an exclude-any label the host matches (or an include-any label it does not match) still installs during setup experience. Assert with a host that would be rejected by IsInHouseAppLabelScoped, so a future reintroduction of the check fails the test.
  • The same title still rejects that host on a manual install from Host details, confirming the scope check was skipped only on the setup path.

Managed app configuration:

  • Stored configuration and $FLEET_VAR_* substitution are applied to the setup-experience install, not just post-enrollment installs — the substitution happens in activateNextInHouseAppInstallActivity (server/datastore/mysql/activities.go:1546-1553), so this should follow, but assert it in tests.
  • An unresolvable variable fails the item with the user-facing reason rather than wedging setup.

Tests: MYSQL_TEST=1 go test ./server/datastore/mysql/..., MYSQL_TEST=1 REDIS_TEST=1 go test ./server/service/... ./server/worker/... ./ee/server/service/.... Add coverage to server/service/integration_mdm_setup_experience_test.go for the iOS .ipa path end to end.

Metadata

Metadata

Assignees

Labels

#g-apple-at-workProduct group focused on Apple devices~sub-taskA technical sub-task that is part of a story. (Not QA'd. Not estimated.)

Type

No type

Projects

Status
✅ Ready for release

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions