Skip to content

Commit 606df3f

Browse files
authored
Upcoming Activities feature branch (#25450)
1 parent e713b44 commit 606df3f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+3758
-1285
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
* Added script execution to the new `upcoming_activities` table.
2+
* Added software installs to the new `upcoming_activities` table.
3+
* Added vpp apps installs to the new `upcoming_activities` table.
4+
* Updated the list upcoming activities endpoint to use the new `upcoming_activities` table as source of truth.
5+
* Added support to activate the next activity when one is enqueued or when one is completed.

cmd/fleet/serve.go

+1
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,7 @@ the way that the Fleet server works.
511511
} else {
512512
mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger)
513513
}
514+
mds.WithPusher(mdmPushService)
514515

515516
checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) {
516517
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names, nil)

ee/server/service/setup_experience.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string
178178
case len(installersPending) > 0:
179179
// enqueue installers
180180
for _, installer := range installersPending {
181-
installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, false, nil)
181+
installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, fleet.HostSoftwareInstallOptions{
182+
SelfService: false,
183+
ForSetupExperience: true,
184+
})
182185
if err != nil {
183186
return false, ctxerr.Wrap(ctx, err, "queueing setup experience install request")
184187
}
@@ -207,7 +210,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string
207210
},
208211
}
209212

210-
cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, false)
213+
cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, fleet.HostSoftwareInstallOptions{
214+
SelfService: false,
215+
ForSetupExperience: true,
216+
})
211217
if err != nil {
212218
return false, ctxerr.Wrap(ctx, err, "queueing vpp app installation")
213219
}
@@ -224,9 +230,12 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string
224230
return false, ctxerr.Errorf(ctx, "setup experience script missing content id: %d", *script.SetupExperienceScriptID)
225231
}
226232
req := &fleet.HostScriptRequestPayload{
227-
HostID: host.ID,
228-
ScriptName: script.Name,
229-
ScriptContentID: *script.ScriptContentID,
233+
HostID: host.ID,
234+
ScriptName: script.Name,
235+
ScriptContentID: *script.ScriptContentID,
236+
// because the script execution request is associated with setup experience,
237+
// it will be enqueued with a higher priority and will run before other
238+
// items in the queue.
230239
SetupExperienceScriptID: script.SetupExperienceScriptID,
231240
}
232241
res, err := svc.ds.NewHostScriptExecutionRequest(ctx, req)

ee/server/service/setup_experience_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func TestSetupExperienceNextStep(t *testing.T) {
5656
return mockListHostsLite, nil
5757
}
5858

59-
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, selfService bool, policyID *uint) (string, error) {
59+
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string, error) {
6060
requestedInstalls[hostID] = append(requestedInstalls[hostID], softwareInstallerID)
6161
return "install-uuid", nil
6262
}

ee/server/service/software_installers.go

+32-40
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616

1717
"github.com/fleetdm/fleet/v4/pkg/file"
1818
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
19-
"github.com/fleetdm/fleet/v4/server/authz"
2019
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
2120
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2221
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
@@ -1017,17 +1016,19 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
10171016
}
10181017
}
10191018

1020-
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, false)
1019+
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
1020+
SelfService: false,
1021+
})
10211022
return err
10221023
}
10231024

1024-
func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, selfService bool) (string, error) {
1025+
func (svc *Service) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, opts fleet.HostSoftwareInstallOptions) (string, error) {
10251026
token, err := svc.GetVPPTokenIfCanInstallVPPApps(ctx, appleDevice, host)
10261027
if err != nil {
10271028
return "", err
10281029
}
10291030

1030-
return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, selfService, nil)
1031+
return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, opts)
10311032
}
10321033

10331034
func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) {
@@ -1073,7 +1074,7 @@ func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDev
10731074
return token, nil
10741075
}
10751076

1076-
func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, selfService bool, policyID *uint) (string, error) {
1077+
func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, token string, opts fleet.HostSoftwareInstallOptions) (string, error) {
10771078
// at this moment, neither the UI nor the back-end are prepared to
10781079
// handle [asyncronous errors][1] on assignment, so before assigning a
10791080
// device to a license, we need to:
@@ -1137,14 +1138,19 @@ func (svc *Service) InstallVPPAppPostValidation(ctx context.Context, host *fleet
11371138
}
11381139
}
11391140

1140-
// add command to install
1141-
cmdUUID := uuid.NewString()
1142-
err = svc.mdmAppleCommander.InstallApplication(ctx, []string{host.UUID}, cmdUUID, vppApp.AdamID)
1143-
if err != nil {
1144-
return "", ctxerr.Wrapf(ctx, err, "sending command to install VPP %s application to host with serial %s", vppApp.AdamID, host.HardwareSerial)
1145-
}
1141+
// TODO(mna): should we associate the device (give the license) only when the
1142+
// upcoming activity is ready to run? I don't think so, because then it could
1143+
// fail when it's ready to run which is probably a worse UX as once enqueued
1144+
// you expect it to succeed. But eventually, we should do better management
1145+
// of the licenses, e.g. if the upcoming activity gets cancelled, it should
1146+
// release the reserved license.
1147+
//
1148+
// But the command is definitely not enqueued now, only when activating the
1149+
// activity.
11461150

1147-
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, selfService, policyID)
1151+
// enqueue the VPP app command to install
1152+
cmdUUID := uuid.NewString()
1153+
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, opts)
11481154
if err != nil {
11491155
return "", ctxerr.Wrapf(ctx, err, "inserting host vpp software install for host with serial %s and app with adamID %s", host.HardwareSerial, vppApp.AdamID)
11501156
}
@@ -1170,7 +1176,9 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
11701176
}
11711177
}
11721178

1173-
_, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, false, nil)
1179+
_, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, fleet.HostSoftwareInstallOptions{
1180+
SelfService: false,
1181+
})
11741182
return ctxerr.Wrap(ctx, err, "inserting software install request")
11751183
}
11761184

@@ -1260,8 +1268,9 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
12601268
}
12611269
}
12621270

1263-
// Get the uninstall script and use the standard script infrastructure to run it.
1264-
contents, err := svc.ds.GetAnyScriptContents(ctx, installer.UninstallScriptContentID)
1271+
// Get the uninstall script to validate there is one, will use the standard
1272+
// script infrastructure to run it.
1273+
_, err = svc.ds.GetAnyScriptContents(ctx, installer.UninstallScriptContentID)
12651274
if err != nil {
12661275
if fleet.IsNotFound(err) {
12671276
return ctxerr.Wrap(ctx,
@@ -1271,32 +1280,11 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
12711280
return err
12721281
}
12731282

1274-
var teamID uint
1275-
if host.TeamID != nil {
1276-
teamID = *host.TeamID
1277-
}
1278-
// create the script execution request; the host will be notified of the
1279-
// script execution request via the orbit config's Notifications mechanism.
1280-
request := fleet.HostScriptRequestPayload{
1281-
HostID: host.ID,
1282-
ScriptContents: string(contents),
1283-
ScriptContentID: installer.UninstallScriptContentID,
1284-
TeamID: teamID,
1285-
}
1286-
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil {
1287-
request.UserID = &ctxUser.ID
1288-
}
1289-
scriptResult, err := svc.ds.NewInternalScriptExecutionRequest(ctx, &request)
1290-
if err != nil {
1291-
return ctxerr.Wrap(ctx, err, "create script execution request")
1292-
}
1293-
1294-
// Update the host software installs table with the uninstall request.
12951283
// Pending uninstalls will automatically show up in the UI Host Details -> Activity -> Upcoming tab.
1296-
if err = svc.insertSoftwareUninstallRequest(ctx, scriptResult.ExecutionID, host, installer); err != nil {
1284+
execID := uuid.NewString()
1285+
if err = svc.insertSoftwareUninstallRequest(ctx, execID, host, installer); err != nil {
12971286
return err
12981287
}
1299-
13001288
return nil
13011289
}
13021290

@@ -1834,7 +1822,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f
18341822
}
18351823
}
18361824

1837-
_, err = svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, true, nil)
1825+
_, err = svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, fleet.HostSoftwareInstallOptions{
1826+
SelfService: true,
1827+
})
18381828
return ctxerr.Wrap(ctx, err, "inserting self-service software install request")
18391829
}
18401830

@@ -1879,7 +1869,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f
18791869
platform := host.FleetPlatform()
18801870
mobileAppleDevice := fleet.AppleDevicePlatform(platform) == fleet.IOSPlatform || fleet.AppleDevicePlatform(platform) == fleet.IPadOSPlatform
18811871

1882-
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, true)
1872+
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
1873+
SelfService: true,
1874+
})
18831875
return err
18841876
}
18851877

ee/server/service/software_installers_test.go

+1-7
Original file line numberDiff line numberDiff line change
@@ -105,20 +105,14 @@ func TestInstallUninstallAuth(t *testing.T) {
105105
ds.GetHostLastInstallDataFunc = func(ctx context.Context, hostID uint, installerID uint) (*fleet.HostLastInstallData, error) {
106106
return nil, nil
107107
}
108-
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool, policyID *uint) (string,
108+
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string,
109109
error,
110110
) {
111111
return "request_id", nil
112112
}
113113
ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) {
114114
return []byte("script"), nil
115115
}
116-
ds.NewInternalScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult,
117-
error) {
118-
return &fleet.HostScriptResult{
119-
ExecutionID: "execution_id",
120-
}, nil
121-
}
122116
ds.InsertSoftwareUninstallRequestFunc = func(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
123117
return nil
124118
}

frontend/__mocks__/activityMock.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const DEFAULT_ACTIVITY_MOCK: IActivity = {
77
actor_id: 1,
88
actor_gravatar: "",
99
actor_email: "[email protected]",
10+
fleet_initiated: false,
1011
type: ActivityType.EditedAgentOptions,
1112
};
1213

frontend/components/ActivityItem/ActivityItem.tsx

+2-16
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from "react";
22
import ReactTooltip from "react-tooltip";
33
import classnames from "classnames";
44

5-
import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity";
5+
import { IActivity, IActivityDetails } from "interfaces/activity";
66
import {
77
addGravatarUrlToResource,
88
internationalTimeFormat,
@@ -108,20 +108,6 @@ const ActivityItem = ({
108108
onCancel();
109109
};
110110

111-
// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in
112-
// the backend. For now, if all these fields are empty, then we assume it was
113-
// Fleet-initiated.
114-
let fleetInitiated = false;
115-
if (
116-
!activity.actor_email &&
117-
!activity.actor_full_name &&
118-
(activity.type === ActivityType.InstalledSoftware ||
119-
activity.type === ActivityType.InstalledAppStoreApp ||
120-
activity.type === ActivityType.RanScript)
121-
) {
122-
fleetInitiated = true;
123-
}
124-
125111
return (
126112
<div className={classNames}>
127113
<div className={`${baseClass}__avatar-wrapper`}>
@@ -131,7 +117,7 @@ const ActivityItem = ({
131117
user={{ gravatar_url }}
132118
size="small"
133119
hasWhiteBackground
134-
useFleetAvatar={fleetInitiated}
120+
useFleetAvatar={activity.fleet_initiated}
135121
/>
136122
<div className={`${baseClass}__avatar-lower-dash`} />
137123
</div>

frontend/interfaces/activity.ts

+1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ export interface IActivity {
129129
actor_gravatar: string;
130130
actor_email?: string;
131131
type: ActivityType;
132+
fleet_initiated: boolean;
132133
details?: IActivityDetails;
133134
}
134135

frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx

-12
Original file line numberDiff line numberDiff line change
@@ -1331,18 +1331,6 @@ const GlobalActivityItem = ({
13311331
}: IActivityItemProps) => {
13321332
const hasDetails = ACTIVITIES_WITH_DETAILS.has(activity.type);
13331333

1334-
// Add the "Fleet" name to the activity if needed.
1335-
// TODO: remove/refactor this once we have "fleet-initiated" activities.
1336-
if (
1337-
!activity.actor_email &&
1338-
!activity.actor_full_name &&
1339-
(activity.type === ActivityType.InstalledSoftware ||
1340-
activity.type === ActivityType.InstalledAppStoreApp ||
1341-
activity.type === ActivityType.RanScript)
1342-
) {
1343-
activity.actor_full_name = "Fleet";
1344-
}
1345-
13461334
const renderActivityPrefix = () => {
13471335
const DEFAULT_ACTOR_DISPLAY = <b>{activity.actor_full_name} </b>;
13481336

frontend/pages/hosts/details/cards/Activity/PastActivityFeed/PastActivityFeed.tsx

+1-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22

3-
import { ActivityType, IHostPastActivity } from "interfaces/activity";
3+
import { IHostPastActivity } from "interfaces/activity";
44
import { IHostPastActivitiesResponse } from "services/entities/activities";
55

66
// @ts-ignore
@@ -54,18 +54,6 @@ const PastActivityFeed = ({
5454
<div className={baseClass}>
5555
<div>
5656
{activitiesList.map((activity: IHostPastActivity) => {
57-
// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in
58-
// the backend. For now, if all these fields are empty, then we assume it was
59-
// Fleet-initiated.
60-
if (
61-
!activity.actor_email &&
62-
!activity.actor_full_name &&
63-
(activity.type === ActivityType.InstalledSoftware ||
64-
activity.type === ActivityType.InstalledAppStoreApp ||
65-
activity.type === ActivityType.RanScript)
66-
) {
67-
activity.actor_full_name = "Fleet";
68-
}
6957
const ActivityItemComponent = pastActivityComponentMap[activity.type];
7058
return (
7159
<ActivityItemComponent

frontend/pages/hosts/details/cards/Activity/UpcomingActivityFeed/UpcomingActivityFeed.tsx

+1-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React from "react";
22

3-
import { ActivityType, IHostUpcomingActivity } from "interfaces/activity";
3+
import { IHostUpcomingActivity } from "interfaces/activity";
44
import { IHostUpcomingActivitiesResponse } from "services/entities/activities";
55

66
// @ts-ignore
@@ -55,18 +55,6 @@ const UpcomingActivityFeed = ({
5555
<div className={baseClass}>
5656
<div className={`${baseClass}__feed-list`}>
5757
{activitiesList.map((activity: IHostUpcomingActivity) => {
58-
// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in
59-
// the backend. For now, if all these fields are empty, then we assume it was
60-
// Fleet-initiated.
61-
if (
62-
!activity.actor_email &&
63-
!activity.actor_full_name &&
64-
(activity.type === ActivityType.InstalledSoftware ||
65-
activity.type === ActivityType.InstalledAppStoreApp ||
66-
activity.type === ActivityType.RanScript)
67-
) {
68-
activity.actor_full_name = "Fleet";
69-
}
7058
const ActivityItemComponent =
7159
upcomingActivityComponentMap[activity.type];
7260
return (

0 commit comments

Comments
 (0)