From 8a8b8403b257df501caa2e6a76cfd2598487754b Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Tue, 19 Nov 2024 17:38:09 -0500 Subject: [PATCH] fix: show script name in activity for setup experience script (#23944) > Related issue: #23787 This adds the script name to both the upcoming and past activities. Demo video: https://www.youtube.com/watch?v=kLSsUZhyMC4 # 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/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/updated tests - [x] Manual QA for all new/changed functionality --- changes/23787-script-name | 2 + ee/server/service/setup_experience.go | 7 +- server/datastore/mysql/activities.go | 4 +- server/datastore/mysql/activities_test.go | 26 +++- server/datastore/mysql/scripts.go | 8 +- server/datastore/mysql/setup_experience.go | 26 ++++ .../datastore/mysql/setup_experience_test.go | 27 +++- server/fleet/datastore.go | 32 +++++ server/fleet/scripts.go | 7 + server/mock/datastore_mock.go | 12 ++ server/service/integration_mdm_dep_test.go | 130 +++++++++++++++--- server/service/orbit.go | 11 +- 12 files changed, 256 insertions(+), 36 deletions(-) create mode 100644 changes/23787-script-name diff --git a/changes/23787-script-name b/changes/23787-script-name new file mode 100644 index 0000000000..af50855bad --- /dev/null +++ b/changes/23787-script-name @@ -0,0 +1,2 @@ +- Fixes a bug where the name of the setup experience script was not showing up in the activity for + that script execution. \ No newline at end of file diff --git a/ee/server/service/setup_experience.go b/ee/server/service/setup_experience.go index 077630d3c4..7bece3103e 100644 --- a/ee/server/service/setup_experience.go +++ b/ee/server/service/setup_experience.go @@ -220,9 +220,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string return false, ctxerr.Errorf(ctx, "setup experience script missing content id: %d", *script.SetupExperienceScriptID) } req := &fleet.HostScriptRequestPayload{ - HostID: host.ID, - ScriptName: script.Name, - ScriptContentID: *script.ScriptContentID, + HostID: host.ID, + ScriptName: script.Name, + ScriptContentID: *script.ScriptContentID, + SetupExperienceScriptID: script.SetupExperienceScriptID, } res, err := svc.ds.NewHostScriptExecutionRequest(ctx, req) if err != nil { diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index cdbf3b440b..bfe732426e 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -312,7 +312,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint JSON_OBJECT( 'host_id', hsr.host_id, 'host_display_name', COALESCE(hdn.display_name, ''), - 'script_name', COALESCE(scr.name, ''), + 'script_name', COALESCE(ses.name, COALESCE(scr.name, '')), 'script_execution_id', hsr.execution_id, 'async', NOT hsr.sync_request, 'policy_id', hsr.policy_id, @@ -330,6 +330,8 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint scripts scr ON scr.id = hsr.script_id LEFT OUTER JOIN host_software_installs hsi ON hsi.execution_id = hsr.execution_id + LEFT OUTER JOIN + setup_experience_scripts ses ON ses.id = hsr.setup_experience_script_id WHERE hsr.host_id = :host_id AND hsr.exit_code IS NULL AND diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go index 2e19fddccf..ce3a33815b 100644 --- a/server/datastore/mysql/activities_test.go +++ b/server/datastore/mysql/activities_test.go @@ -541,6 +541,15 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { h2SelfService, err := ds.InsertSoftwareInstallRequest(noUserCtx, h2.ID, sw1Meta.InstallerID, true, nil) require.NoError(t, err) + setupExpScript := &fleet.Script{Name: "setup_experience_script", ScriptContents: "setup_experience"} + err = ds.SetSetupExperienceScript(ctx, setupExpScript) + require.NoError(t, err) + ses, err := ds.GetSetupExperienceScript(ctx, h2.TeamID) + require.NoError(t, err) + hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h2.ID, ScriptContents: "setup_experience", SetupExperienceScriptID: &ses.ID}) + require.NoError(t, err) + h2SetupExp := hsr.ExecutionID + // create pending install and uninstall requests for h3 that will be deleted _, err = ds.InsertSoftwareInstallRequest(ctx, h3.ID, sw3Meta.InstallerID, false, nil) require.NoError(t, err) @@ -560,7 +569,8 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2SelfService) endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2Bar) endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2A, h2F) - SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_vpp_software_installs", "command_uuid", vppCommand1, vppCommand2) + endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_vpp_software_installs", "command_uuid", vppCommand1, vppCommand2) + SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2SetupExp) execIDsWithUser := map[string]bool{ h1A: true, @@ -576,11 +586,13 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { h2Bar: true, vppCommand1: true, vppCommand2: false, + h2SetupExp: false, } execIDsScriptName := map[string]string{ - h1A: scr1.Name, - h1B: scr2.Name, - h2A: scr1.Name, + h1A: scr1.Name, + h1B: scr2.Name, + h2A: scr1.Name, + h2SetupExp: setupExpScript.Name, } execIDsSoftwareTitle := map[string]string{ h1Fleet: "foo", @@ -641,10 +653,10 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8}, }, { - opts: fleet.ListOptions{PerPage: 4}, + opts: fleet.ListOptions{PerPage: 5}, hostID: h2.ID, - wantExecs: []string{h2SelfService, h2Bar, h2A, vppCommand2}, - wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 4}, + wantExecs: []string{h2SelfService, h2Bar, h2A, vppCommand2, h2SetupExp}, + wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 5}, }, { opts: fleet.ListOptions{}, diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go index d09b616455..b6e4ff0576 100644 --- a/server/datastore/mysql/scripts.go +++ b/server/datastore/mysql/scripts.go @@ -39,8 +39,8 @@ func (ds *Datastore) NewHostScriptExecutionRequest(ctx context.Context, request func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) { const ( - insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?, ?)` - getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?` + insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request, setup_experience_script_id) VALUES (?, ?, ?, '', ?, ?, ?, ?, ?)` + getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents, hsr.setup_experience_script_id FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?` ) execID := uuid.New().String() @@ -52,6 +52,7 @@ func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, requ request.PolicyID, request.UserID, request.SyncRequest, + request.SetupExperienceScriptID, ) if err != nil { return nil, ctxerr.Wrap(ctx, err, "new host script execution request") @@ -269,7 +270,8 @@ func (ds *Datastore) getHostScriptExecutionResultDB(ctx context.Context, q sqlx. hsr.created_at, hsr.user_id, hsr.sync_request, - hsr.host_deleted_at + hsr.host_deleted_at, + hsr.setup_experience_script_id FROM host_script_results hsr JOIN diff --git a/server/datastore/mysql/setup_experience.go b/server/datastore/mysql/setup_experience.go index 328cbb12ae..fe69a14aef 100644 --- a/server/datastore/mysql/setup_experience.go +++ b/server/datastore/mysql/setup_experience.go @@ -404,6 +404,32 @@ WHERE return &script, nil } +func (ds *Datastore) GetSetupExperienceScriptByID(ctx context.Context, scriptID uint) (*fleet.Script, error) { + query := ` +SELECT + id, + team_id, + name, + script_content_id, + created_at, + updated_at +FROM + setup_experience_scripts +WHERE + id = ? +` + + var script fleet.Script + if err := sqlx.GetContext(ctx, ds.reader(ctx), &script, query, scriptID); err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("SetupExperienceScript"), "get setup experience script by id") + } + return nil, ctxerr.Wrap(ctx, err, "get setup experience script by id") + } + + return &script, nil +} + func (ds *Datastore) SetSetupExperienceScript(ctx context.Context, script *fleet.Script) error { err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { var err error diff --git a/server/datastore/mysql/setup_experience_test.go b/server/datastore/mysql/setup_experience_test.go index fa48a4107c..f1f387fe1a 100644 --- a/server/datastore/mysql/setup_experience_test.go +++ b/server/datastore/mysql/setup_experience_test.go @@ -29,6 +29,7 @@ func TestSetupExperience(t *testing.T) { {"ListSetupExperienceStatusResults", testSetupExperienceStatusResults}, {"SetupExperienceScriptCRUD", testSetupExperienceScriptCRUD}, {"TestHostInSetupExperience", testHostInSetupExperience}, + {"TestGetSetupExperienceScriptByID", testGetSetupExperienceScriptByID}, } for _, c := range cases { @@ -675,7 +676,6 @@ func testSetSetupExperienceTitles(t *testing.T, ds *Datastore) { assert.False(t, *titles[0].SoftwarePackage.InstallDuringSetup) assert.False(t, *titles[1].SoftwarePackage.InstallDuringSetup) assert.False(t, *titles[2].AppStoreApp.InstallDuringSetup) - } func testSetupExperienceStatusResults(t *testing.T, ds *Datastore) { @@ -909,3 +909,28 @@ func testHostInSetupExperience(t *testing.T, ds *Datastore) { require.NoError(t, err) require.False(t, inSetupExperience) } + +func testGetSetupExperienceScriptByID(t *testing.T, ds *Datastore) { + ctx := context.Background() + + script := &fleet.Script{ + Name: "setup_experience_script", + ScriptContents: "echo hello", + } + + err := ds.SetSetupExperienceScript(ctx, script) + require.NoError(t, err) + + scriptByTeamID, err := ds.GetSetupExperienceScript(ctx, nil) + require.NoError(t, err) + + gotScript, err := ds.GetSetupExperienceScriptByID(ctx, scriptByTeamID.ID) + require.NoError(t, err) + + require.Equal(t, script.Name, gotScript.Name) + require.NotZero(t, gotScript.ScriptContentID) + + b, err := ds.GetAnyScriptContents(ctx, gotScript.ScriptContentID) + require.NoError(t, err) + require.Equal(t, script.ScriptContents, string(b)) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index b1bcc4b7b2..496c707b86 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1795,14 +1795,46 @@ type Datastore interface { // Setup Experience // + // ListSetupExperienceResultsByHostUUID lists the setup experience results for a host by its UUID. ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string) ([]*SetupExperienceStatusResult, error) + + // UpdateSetupExperienceStatusResult updates the given setup experience status result. UpdateSetupExperienceStatusResult(ctx context.Context, status *SetupExperienceStatusResult) error + + // EnqueueSetupExperienceItems enqueues the relevant setup experience items (software and + // script) for a given host. It first clears out any pre-existing setup experience items that + // were previously enqueued for the host (since the setup experience only happens once during + // the initial device setup). It then adds any software and script that have been configured for + // this team to the host's queue and sets their status to pending. If any items were enqueued, + // it returns true, otherwise it returns false. EnqueueSetupExperienceItems(ctx context.Context, hostUUID string, teamID uint) (bool, error) + + // GetSetupExperienceScript gets the setup experience script for a team. There can only be 1 + // setup experience script per team. GetSetupExperienceScript(ctx context.Context, teamID *uint) (*Script, error) + + // GetSetupExperienceScriptByID gets the setup experience script by its ID. + GetSetupExperienceScriptByID(ctx context.Context, scriptID uint) (*Script, error) + + // SetSetupExperienceScript sets the setup experience script to the given script. SetSetupExperienceScript(ctx context.Context, script *Script) error + + // DeleteSetupExperienceScript deletes the setup experience script for the given team. DeleteSetupExperienceScript(ctx context.Context, teamID *uint) error + + // MaybeUpdateSetupExperienceScriptStatus updates the status of the setup experience script for + // the given host if the script result row exists. If there was an update, it returns true. + // Otherwise, it returns false. MaybeUpdateSetupExperienceScriptStatus(ctx context.Context, hostUUID string, executionID string, status SetupExperienceStatusResultStatus) (bool, error) + + // MaybeUpdateSetupExperienceSoftwareInstallStatus updates the status of the setup experience + // software installer for the given host if the software installer result row exists. If there + // was an update, it returns true. Otherwise, it returns false. MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx context.Context, hostUUID string, executionID string, status SetupExperienceStatusResultStatus) (bool, error) + + // MaybeUpdateSetupExperienceVPPStatus updates the status of the setup experience + // VPP app for the given host if the VPP app installer row exists. If there was an update, it + // returns true. Otherwise, it returns false. MaybeUpdateSetupExperienceVPPStatus(ctx context.Context, hostUUID string, commandUUID string, status SetupExperienceStatusResultStatus) (bool, error) // Fleet-maintained apps diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go index 28bba6ad8c..8b011642b1 100644 --- a/server/fleet/scripts.go +++ b/server/fleet/scripts.go @@ -149,6 +149,9 @@ type HostScriptRequestPayload struct { // SyncRequest is filled automatically based on the endpoint used to create // the execution request (synchronous or asynchronous). SyncRequest bool `json:"-"` + // SetupExperienceScriptID is the ID of the setup experience script related to this request + // payload, if such a script exists. + SetupExperienceScriptID *uint `json:"-"` } func (r HostScriptRequestPayload) ValidateParams(waitForResult time.Duration) error { @@ -251,6 +254,10 @@ type HostScriptResult struct { // results can still be returned to see activity details after the host got // deleted. HostDeletedAt *time.Time `json:"-" db:"host_deleted_at"` + + // SetupExperienceScriptID is the ID of the setup experience script, if this script execution + // was part of setup experience. + SetupExperienceScriptID *uint `json:"-" db:"setup_experience_script_id"` } func (hsr HostScriptResult) AuthzType() string { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index d793428ad7..51bda642d3 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1141,6 +1141,8 @@ type EnqueueSetupExperienceItemsFunc func(ctx context.Context, hostUUID string, type GetSetupExperienceScriptFunc func(ctx context.Context, teamID *uint) (*fleet.Script, error) +type GetSetupExperienceScriptByIDFunc func(ctx context.Context, scriptID uint) (*fleet.Script, error) + type SetSetupExperienceScriptFunc func(ctx context.Context, script *fleet.Script) error type DeleteSetupExperienceScriptFunc func(ctx context.Context, teamID *uint) error @@ -2844,6 +2846,9 @@ type DataStore struct { GetSetupExperienceScriptFunc GetSetupExperienceScriptFunc GetSetupExperienceScriptFuncInvoked bool + GetSetupExperienceScriptByIDFunc GetSetupExperienceScriptByIDFunc + GetSetupExperienceScriptByIDFuncInvoked bool + SetSetupExperienceScriptFunc SetSetupExperienceScriptFunc SetSetupExperienceScriptFuncInvoked bool @@ -6800,6 +6805,13 @@ func (s *DataStore) GetSetupExperienceScript(ctx context.Context, teamID *uint) return s.GetSetupExperienceScriptFunc(ctx, teamID) } +func (s *DataStore) GetSetupExperienceScriptByID(ctx context.Context, scriptID uint) (*fleet.Script, error) { + s.mu.Lock() + s.GetSetupExperienceScriptByIDFuncInvoked = true + s.mu.Unlock() + return s.GetSetupExperienceScriptByIDFunc(ctx, scriptID) +} + func (s *DataStore) SetSetupExperienceScript(ctx context.Context, script *fleet.Script) error { s.mu.Lock() s.SetSetupExperienceScriptFuncInvoked = true diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index d2a32c8e33..c03e36c4a5 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -1952,6 +1952,7 @@ func (s *integrationMDMTestSuite) createTeamDeviceForSetupExperienceWithProfileS require.Len(t, listHostsRes.Hosts, 1) require.Equal(t, listHostsRes.Hosts[0].HardwareSerial, teamDevice.SerialNumber) enrolledHost := listHostsRes.Hosts[0].Host + enrolledHost.TeamID = &tm.ID // transfer it to the team s.Do("POST", "/api/v1/fleet/hosts/transfer", @@ -2076,6 +2077,57 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID) + // 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) + require.Len(t, results, 2) + require.NoError(t, err) + var installUUID string + for _, r := range results { + if r.HostSoftwareInstallsExecutionID != nil { + installUUID = *r.HostSoftwareInstallsExecutionID + } + } + + require.NotEmpty(t, installUUID) + + // 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)) + require.NotNil(t, getSoftwareTitleResp.SoftwareTitle) + require.NotNil(t, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage) + + debugPrintActivities := func(activities []*fleet.Activity) []string { + var res []string + for _, activity := range activities { + res = append(res, fmt.Sprintf("%+v", activity)) + } + return res + } + + // Check upcoming activities: we should only have the software upcoming because we don't run the + // script until after the software is done + var hostActivitiesResp listHostUpcomingActivitiesResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", enrolledHost.ID), + nil, http.StatusOK, &hostActivitiesResp) + + expectedActivityDetail := fmt.Sprintf(` + { + "status": "pending_install", + "host_id": %d, + "policy_id": null, + "policy_name": null, + "install_uuid": "%s", + "self_service": false, + "software_title": "%s", + "software_package": "%s", + "host_display_name": "%s" + } + `, enrolledHost.ID, installUUID, getSoftwareTitleResp.SoftwareTitle.Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, enrolledHost.DisplayName()) + require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", debugPrintActivities(hostActivitiesResp.Activities)) + require.NotNil(t, hostActivitiesResp.Activities[0].Details) + require.JSONEq(t, expectedActivityDetail, string(*hostActivitiesResp.Activities[0].Details)) + // no MDM command got enqueued due to the /status call (device not released yet) cmd, err = mdmDevice.Idle() require.NoError(t, err) @@ -2093,20 +2145,6 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu require.Equal(t, "script.sh", statusResp.Results.Script.Name) require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.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) - require.Len(t, results, 2) - require.NoError(t, err) - var installUUID string - for _, r := range results { - if r.HostSoftwareInstallsExecutionID != nil { - installUUID = *r.HostSoftwareInstallsExecutionID - } - } - - require.NotEmpty(t, installUUID) - // record a result for software installation s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{ @@ -2137,12 +2175,10 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu // Software is installed, now we should run the script statusResp = 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 require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) require.Equal(t, fleet.SetupExperienceStatusSuccess, 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) @@ -2158,6 +2194,48 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu } } + // Validate past activity for software install + // For some reason the display name that's included in the `enrolledHost` is _slightly_ + // different than the expected value in the activities. Pulling the host directly gets the + // correct display name. + var getHostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", enrolledHost.ID), nil, http.StatusOK, &getHostResp) + + expectedActivityDetail = fmt.Sprintf(` +{ + "host_id": %d, + "host_display_name": "%s", + "software_title": "%s", + "software_package": "%s", + "self_service": false, + "install_uuid": "%s", + "status": "installed", + "policy_id": null, + "policy_name": null +} + `, enrolledHost.ID, getHostResp.Host.DisplayName, statusResp.Results.Software[0].Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, installUUID) + + s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), expectedActivityDetail, 0) + + // Validate upcoming activity for the script + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", enrolledHost.ID), + nil, http.StatusOK, &hostActivitiesResp) + + expectedActivityDetail = fmt.Sprintf(` +{ + "async": true, + "host_id": %d, + "policy_id": null, + "policy_name": null, + "script_name": "%s", + "host_display_name": "%s", + "script_execution_id": "%s" +} + `, enrolledHost.ID, statusResp.Results.Script.Name, enrolledHost.DisplayName(), execID) + require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", debugPrintActivities(hostActivitiesResp.Activities)) + require.NotNil(t, hostActivitiesResp.Activities[0].Details) + require.JSONEq(t, expectedActivityDetail, string(*hostActivitiesResp.Activities[0].Details)) + // record a result for script execution var scriptResp orbitPostScriptResultResponse s.DoJSON("POST", "/api/fleet/orbit/scripts/result", @@ -2168,7 +2246,6 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu // release of the device, as all setup experience steps are now complete. statusResp = 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 require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) @@ -2202,6 +2279,21 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu } require.Equal(t, 1, deviceConfiguredCount) require.Equal(t, 0, otherCount) + + // Validate activity for script run + expectedActivityDetail = fmt.Sprintf(` +{ + "async": true, + "host_id": %d, + "policy_id": null, + "policy_name": null, + "script_name": "%s", + "host_display_name": "%s", + "script_execution_id": "%s" +} + `, enrolledHost.ID, statusResp.Results.Script.Name, getHostResp.Host.DisplayName, execID) + + s.lastActivityMatches(fleet.ActivityTypeRanScript{}.ActivityName(), expectedActivityDetail, 0) } func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptForceRelease() { @@ -2360,7 +2452,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo require.Equal(t, 0, otherCount) } -func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM() { +func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingItFromABM() { t := s.T() s.enableABM(t.Name()) ctx := context.Background() @@ -2534,7 +2626,6 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM( {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()}, } - t.Log("RUN AFTER DELETED") s.runDEPSchedule() a := checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) @@ -2550,7 +2641,6 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM( {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now(), ProfileUUID: a[mdmDevice.SerialNumber].ProfileUUID}, } - t.Log("RUN AFTER RE-ADDED") s.runDEPSchedule() a = checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) diff --git a/server/service/orbit.go b/server/service/orbit.go index 5ad9b0dc3b..69dd27a882 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -837,11 +837,20 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host } } var scriptName string - if hsr.ScriptID != nil { + + switch { + case hsr.ScriptID != nil: scr, err := svc.ds.Script(ctx, *hsr.ScriptID) if err != nil { return ctxerr.Wrap(ctx, err, "get saved script") } + scriptName = scr.Name + case hsr.SetupExperienceScriptID != nil: + scr, err := svc.ds.GetSetupExperienceScriptByID(ctx, *hsr.SetupExperienceScriptID) + if err != nil { + return ctxerr.Wrap(ctx, err, "get setup experience script") + } + scriptName = scr.Name }