Skip to content

Commit

Permalink
feat: macOS setup experience - install software and run a script (#23267
Browse files Browse the repository at this point in the history
)

> Related issue: #19372

These commits have all been reviewed in their respective PRs.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [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/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)
- [x] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.
- [x] Added/updated tests
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes
- [x] If database migrations are included, checked table schema to
confirm autoupdate
- For database migrations:
- [x] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [x] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [x] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).
- [x] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [x] Orbit runs on macOS, Linux and Windows. Check if the orbit
feature/bugfix should only apply to one platform (`runtime.GOOS`).
- [x] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- [x] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).
  • Loading branch information
georgekarrv authored Oct 28, 2024
2 parents 82d88bb + 9f32642 commit d66a2a7
Show file tree
Hide file tree
Showing 163 changed files with 8,467 additions and 486 deletions.
38 changes: 38 additions & 0 deletions articles/macos-setup-experience.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ In Fleet, you can customize the out-of-the-box macOS Setup Assistant with Remote

* Install a bootstrap package to gain full control over the setup experience by installing tools like Puppet, Munki, DEP notify, custom scripts, and more.

* Install software (App Store apps, custom packages, and Fleet-maintained apps).

* Run a script.

In addition to the customization above, Fleet automatically installs the fleetd agent during out-of-the-box macOS setup. This agent is responsible for reporting host vitals to Fleet and presenting Fleet Desktop to the end user.

macOS setup features require connecting Fleet to Apple Business Manager (ABM). Learn how [here](https://fleetdm.com/guides/macos-mdm-setup#apple-business-manager-abm).
Expand Down Expand Up @@ -161,6 +165,40 @@ Testing requires a test Mac that is present in your Apple Business Manager (ABM)
4. Boot up your test Mac and complete the custom out-of-the-box setup experience.
## Software and script
You can configure software installations and a script to be executed during Setup Assistant. This capability allows you to configure your end users' machines during the unboxing experience, speeding up their onboarding and reducing setup time.

If you configure software and/or a script for setup experience, users will see a window like this pop open after their device enrolls in MDM via ADE:

![screen shot of Fleet setup experience window](../website/assets/images/install-software-preview.png)

This window shows the status of the software installations as well as the script exectution. Once all steps have completed, the window can be closed and Setup Assistant will proceed as usual.

### Install software

To configure software to be installed during setup experience:

1. Click on the "Controls" tab in the main navigation bar. Click on "Setup experience", and then on "4. Install software".

2. Click the "Add software" button. In the modal, select the software that you want to have installed during the setup experience. You can search the list of software by using the search bar in the modal. Click "Save" to save your selection and close the modal.

### Run script

To configure a script to run during setup experience:

1. Click on the "Controls" tab in the main navigation bar. Click on "Setup experience", and then on "5. Run script".

2. Click "Upload" and select a script (.sh file) from the file picker modal. Once the script is uploaded, you can use the buttons on the script in the web UI to download or delete the script.

### Configuring via REST API

Fleet also provides a REST API for managing setup experience software and scripts programmatically. Learn more about Fleet's [REST API](https://fleetdm.com/docs/rest-api/rest-api).
### Configuring via GitOps
To manage setup experience software and script using Fleet's best practice GitOps, check out the `macos_setup` key in the GitOps reference documentation [here](https://fleetdm.com/docs/configuration/yaml-files#macos-setup)

<meta name="category" value="guides">
<meta name="authorGitHubUsername" value="noahtalerman">
<meta name="authorFullName" value="Noah Talerman">
Expand Down
Binary file added assets/images/install-software-preview.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions changes/22373-install-software-for-setup-experience-ui
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- add UI for the isntall software setup experience
1 change: 1 addition & 0 deletions changes/22374-add-ui-for-setup-experience-script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- add UI for adding a setup experience script
1 change: 1 addition & 0 deletions changes/22375-setup-experience-migration
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add database migrations to support Setup Experience
1 change: 1 addition & 0 deletions changes/22377-setup-experience-software-api
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add software experience software title selection API
1 change: 1 addition & 0 deletions changes/22379-queue-setup-experience-software
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add integration to queue setup experience software installation on automatic enrollment
1 change: 1 addition & 0 deletions changes/22381-setup-experience-state-machine
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add service to track install status
1 change: 1 addition & 0 deletions changes/22382-prevent-delete-software-used-in-setup
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Added a validation to prevent removing a software package or a VPP app from a team if that software is selected to be installed during the setup experience.
1 change: 1 addition & 0 deletions changes/22385-cli-gitops-macos-setup-software-and-script
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Added support to `fleetctl gitops` to specify a setup experience script to run and software to install, for a team or no team.
2 changes: 2 additions & 0 deletions changes/22637-status
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Adds an Orbit endpoint (`POST /orbit/setup_experience/status`) for checking the status of a macOS
host's setup experience steps.
1 change: 1 addition & 0 deletions changes/22783-release-ade-enrolled-device
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Deprecated the worker-based job to release macOS devices automatically after the setup experience, replace it with the fleetd-specific "/status" endpoint that is polled by the Setup Experience dialog controlled by Fleet during the setup flow.
2 changes: 1 addition & 1 deletion cmd/fleetctl/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ func applyCommand() *cli.Command {
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)

_, _, _, err = fleetClient.ApplyGroup(c.Context, specs, baseDir, logf, nil, opts, teamsSoftwareInstallers, teamsScripts)
_, _, _, err = fleetClient.ApplyGroup(c.Context, false, specs, baseDir, logf, nil, opts, teamsSoftwareInstallers, teamsScripts)
if err != nil {
return err
}
Expand Down
171 changes: 125 additions & 46 deletions cmd/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,9 @@ func TestGitOpsBasicTeam(t *testing.T) {
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}

tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
Expand Down Expand Up @@ -903,6 +906,9 @@ func TestGitOpsFullTeam(t *testing.T) {
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}

startSoftwareInstallerServer(t)

Expand Down Expand Up @@ -1211,6 +1217,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}

ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
var teamsSummary []*fleet.TeamSummary
Expand Down Expand Up @@ -1532,6 +1541,9 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}

globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
Expand Down Expand Up @@ -1852,6 +1864,7 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) {

func TestGitOpsTeamSofwareInstallers(t *testing.T) {
startSoftwareInstallerServer(t)
startAndServeVPPServer(t)

cases := []struct {
file string
Expand All @@ -1871,12 +1884,37 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) {
{"testdata/gitops/team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/team_software_installer_no_url.yml", "software URL is required"},
{"testdata/gitops/team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"},
// team tests for setup experience software/script
{"testdata/gitops/team_setup_software_valid.yml", ""},
{"testdata/gitops/team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
{"testdata/gitops/team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that team"},
{"testdata/gitops/team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that team"},
}
for _, c := range cases {
t.Run(filepath.Base(c.file), func(t *testing.T) {
setupFullGitOpsPremiumServer(t)
ds, _, _ := setupFullGitOpsPremiumServer(t)
tokExpire := time.Now().Add(time.Hour)
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
require.NoError(t, err)

_, err := runAppNoChecks([]string{"gitops", "-f", c.file})
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{
ID: 1,
OrgName: "Fleet",
Location: "Earth",
RenewDate: tokExpire,
Token: string(token),
Teams: nil,
}, nil
}

_, err = runAppNoChecks([]string{"gitops", "-f", c.file})
if c.wantErr == "" {
require.NoError(t, err)
} else {
Expand Down Expand Up @@ -1908,6 +1946,7 @@ func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {

func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
startSoftwareInstallerServer(t)
startAndServeVPPServer(t)

cases := []struct {
noTeamFile string
Expand All @@ -1925,18 +1964,48 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
{"testdata/gitops/no_team_software_installer_post_install_not_found.yml", "no such file or directory"},
{"testdata/gitops/no_team_software_installer_no_url.yml", "software URL is required"},
{"testdata/gitops/no_team_software_installer_invalid_self_service_value.yml", "\"packages.self_service\" must be a bool, found string"},
// No team tests for setup experience software/script
{"testdata/gitops/no_team_setup_software_valid.yml", ""},
{"testdata/gitops/no_team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
{"testdata/gitops/no_team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that team"},
// VPP apps for No Team is unsupported at the moment : https://github.com/fleetdm/fleet/issues/22970
// {"testdata/gitops/no_team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that team"},
}
for _, c := range cases {
t.Run(filepath.Base(c.noTeamFile), func(t *testing.T) {
setupFullGitOpsPremiumServer(t)
ds, _, _ := setupFullGitOpsPremiumServer(t)
tokExpire := time.Now().Add(time.Hour)
token, err := test.CreateVPPTokenEncoded(tokExpire, "fleet", "ca")
require.NoError(t, err)

ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
return nil
}
ds.BatchInsertVPPAppsFunc = func(ctx context.Context, apps []*fleet.VPPApp) error {
return nil
}
ds.GetVPPTokenByTeamIDFunc = func(ctx context.Context, teamID *uint) (*fleet.VPPTokenDB, error) {
return &fleet.VPPTokenDB{
ID: 1,
OrgName: "Fleet",
Location: "Earth",
RenewDate: tokExpire,
Token: string(token),
Teams: nil,
}, nil
}

t.Setenv("APPLE_BM_DEFAULT_TEAM", "")
globalFile := "./testdata/gitops/global_config_no_paths.yml"
if strings.HasPrefix(filepath.Base(c.noTeamFile), "no_team_setup_software") {
// the controls section is in the no-team test file, so use a global file without that section
globalFile = "./testdata/gitops/global_config_no_paths_no_controls.yml"
}
dstPath := filepath.Join(filepath.Dir(c.noTeamFile), "no-team.yml")
t.Cleanup(func() {
os.Remove(dstPath)
})
err := file.Copy(c.noTeamFile, dstPath, 0o755)
err = file.Copy(c.noTeamFile, dstPath, 0o755)
require.NoError(t, err)
_, err = runAppNoChecks([]string{"gitops", "-f", globalFile, "-f", dstPath})
if c.wantErr == "" {
Expand All @@ -1949,48 +2018,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
}

func TestGitOpsTeamVPPApps(t *testing.T) {
config := &appleVPPConfigSrvConf{
Assets: []vpp.Asset{
{
AdamID: "1",
PricingParam: "STDQ",
AvailableCount: 12,
},
{
AdamID: "2",
PricingParam: "STDQ",
AvailableCount: 3,
},
},
SerialNumbers: []string{"123", "456"},
}

startVPPApplyServer(t, config)

appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// a map of apps we can respond with
db := map[string]string{
// macos app
"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
// macos, ios, ipados app
"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
// ipados app
"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
"supportedDevices": ["iPadAir-iPadAir"] }`,
}

adamIDString := r.URL.Query().Get("id")
adamIDs := strings.Split(adamIDString, ",")

var objs []string
for _, a := range adamIDs {
objs = append(objs, db[a])
}

_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
}))
t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)
startAndServeVPPServer(t)

cases := []struct {
file string
Expand Down Expand Up @@ -2262,6 +2290,51 @@ func startVPPApplyServer(t *testing.T, config *appleVPPConfigSrvConf) {
t.Cleanup(srv.Close)
}

func startAndServeVPPServer(t *testing.T) {
config := &appleVPPConfigSrvConf{
Assets: []vpp.Asset{
{
AdamID: "1",
PricingParam: "STDQ",
AvailableCount: 12,
},
{
AdamID: "2",
PricingParam: "STDQ",
AvailableCount: 3,
},
},
SerialNumbers: []string{"123", "456"},
}

startVPPApplyServer(t, config)

appleITunesSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// a map of apps we can respond with
db := map[string]string{
// macos app
"1": `{"bundleId": "a-1", "artworkUrl512": "https://example.com/images/1", "version": "1.0.0", "trackName": "App 1", "TrackID": 1}`,
// macos, ios, ipados app
"2": `{"bundleId": "b-2", "artworkUrl512": "https://example.com/images/2", "version": "2.0.0", "trackName": "App 2", "TrackID": 2,
"supportedDevices": ["MacDesktop-MacDesktop", "iPhone5s-iPhone5s", "iPadAir-iPadAir"] }`,
// ipados app
"3": `{"bundleId": "c-3", "artworkUrl512": "https://example.com/images/3", "version": "3.0.0", "trackName": "App 3", "TrackID": 3,
"supportedDevices": ["iPadAir-iPadAir"] }`,
}

adamIDString := r.URL.Query().Get("id")
adamIDs := strings.Split(adamIDString, ",")

var objs []string
for _, a := range adamIDs {
objs = append(objs, db[a])
}

_, _ = w.Write([]byte(fmt.Sprintf(`{"results": [%s]}`, strings.Join(objs, ","))))
}))
t.Setenv("FLEET_DEV_ITUNES_URL", appleITunesSrv.URL)
}

func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig, map[string]**fleet.Team) {
testCert, testKey, err := apple_mdm.NewSCEPCACertKey()
require.NoError(t, err)
Expand Down Expand Up @@ -2449,6 +2522,12 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
return []*fleet.ABMToken{}, nil
}
ds.DeleteSetupExperienceScriptFunc = func(ctx context.Context, teamID *uint) error {
return nil
}
ds.SetSetupExperienceScriptFunc = func(ctx context.Context, script *fleet.Script) error {
return nil
}

t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
Expand Down
2 changes: 1 addition & 1 deletion cmd/fleetctl/preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,7 @@ Use the stop and reset subcommands to manage the server and dependencies once st
// so pass in the current working directory.
teamsSoftwareInstallers := make(map[string][]fleet.SoftwarePackageResponse)
teamsScripts := make(map[string][]fleet.ScriptResponse)
_, _, _, err = client.ApplyGroup(c.Context, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{}, teamsSoftwareInstallers, teamsScripts)
_, _, _, err = client.ApplyGroup(c.Context, false, specs, ".", logf, nil, fleet.ApplyClientSpecOptions{}, teamsSoftwareInstallers, teamsScripts)
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion cmd/fleetctl/testdata/expectedGetConfigAppConfigJson.json
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null,
"enable_release_device_manually": false
"enable_release_device_manually": false,
"script": null,
"software": null
},
"windows_settings": {
"custom_settings": null
Expand Down
2 changes: 2 additions & 0 deletions cmd/fleetctl/testdata/expectedGetConfigAppConfigYaml.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ spec:
enable_end_user_authentication: false
enable_release_device_manually: false
macos_setup_assistant:
script:
software:
windows_settings:
custom_settings: null
end_user_authentication:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@
"bootstrap_package": null,
"enable_end_user_authentication": false,
"macos_setup_assistant": null,
"enable_release_device_manually": false
"enable_release_device_manually": false,
"script": null,
"software": null
},
"windows_settings": {
"custom_settings": null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ spec:
enable_end_user_authentication: false
enable_release_device_manually: false
macos_setup_assistant:
script:
software:
windows_settings:
custom_settings:
end_user_authentication:
Expand Down
Loading

0 comments on commit d66a2a7

Please sign in to comment.