Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upcoming Activities feature branch #25450

Draft
wants to merge 28 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
2c3fc20
Unified Queue: tentative DB schema, start refactoring scripts (#25215)
mna Jan 8, 2025
c547a7f
Merge branch 'main' into feat-upcoming-activites-queue
ghernandez345 Jan 13, 2025
8aecae4
Unified Queue: insert software installs in the upcoming queue and add…
mna Jan 14, 2025
bfa106a
FIx conflicts
mna Jan 15, 2025
a0feb23
Unified Queue: insert VPP apps in the upcoming queue, adjust list upc…
mna Jan 15, 2025
ab37362
Update software status host filter for upcoming activities feature (#…
gillespi314 Jan 15, 2025
f7945ef
Merge branch 'main' into feat-upcoming-activites-queue
ghernandez345 Jan 16, 2025
4099d64
UI for adding cancel functionality to the host details activity (#25329)
ghernandez345 Jan 16, 2025
519481d
render fleet avatar if activity is fleet initiated (#25510)
ghernandez345 Jan 17, 2025
1968db1
Merge branch 'main' into feat-upcoming-activites-queue
ghernandez345 Jan 17, 2025
55895e6
Merge branch 'main' into feat-upcoming-activites-queue
ghernandez345 Jan 20, 2025
72cf7d3
fix merge conflicts errors in UI
ghernandez345 Jan 20, 2025
f7e97f7
Unified Queue: implement correct fleet-initiated flag and setup exper…
mna Jan 20, 2025
52af856
Merge branch 'main' into feat-upcoming-activites-queue
ghernandez345 Jan 21, 2025
029841c
Update VPP and software install status summaries and filters for unif…
gillespi314 Jan 22, 2025
1cb234f
Unified Queue: trigger next activity when receiving results or enquei…
mna Jan 22, 2025
1093245
Upcoming Queue: list only active pending scripts/software for orbit n…
mna Jan 27, 2025
b3fbb5c
Fix conflicts
mna Jan 27, 2025
e94e4a0
Regen schema
mna Jan 27, 2025
a1b1a93
Move db migration last
mna Jan 27, 2025
55d9b53
Fix build issues in tests
mna Jan 27, 2025
8aad599
Update ds.GetHostLastInstallData for unified queue (#25729)
gillespi314 Jan 27, 2025
523fa6d
Upcoming Queue: fix some more queries for pending state (#25781)
mna Jan 27, 2025
5a217b5
Fix conflicts
mna Jan 28, 2025
70ceb58
Unified Queue: fix get scripts with latest execution query (#25809)
mna Jan 29, 2025
4621e19
Unified Queue: adjust queries in bactch-set scripts, all mysql tests …
mna Jan 29, 2025
f82b806
Merge branch 'main' into feat-upcoming-activites-queue
mna Jan 29, 2025
594d358
Unified Queue: adjust batch-set software installers (#25856)
mna Jan 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changes/23913-upcoming-activities-handle-scripts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
* Added script execution to the new `upcoming_activities` table.
* Added software installs to the new `upcoming_activities` table.
* Added vpp apps installs to the new `upcoming_activities` table.
* Updated the list upcoming activities endpoint to use the new `upcoming_activities` table as source of truth.
* Added support to activate the next activity when one is enqueued or when one is completed.
1 change: 1 addition & 0 deletions cmd/fleet/serve.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ the way that the Fleet server works.
} else {
mdmPushService = nanomdm_pushsvc.New(mdmStorage, mdmStorage, pushProviderFactory, nanoMDMLogger)
}
mds.WithPusher(mdmPushService)

checkMDMAssets := func(names []fleet.MDMAssetName) (bool, error) {
_, err = ds.GetAllMDMConfigAssetsByName(context.Background(), names, nil)
Expand Down
19 changes: 14 additions & 5 deletions ee/server/service/setup_experience.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string
case len(installersPending) > 0:
// enqueue installers
for _, installer := range installersPending {
installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, false, nil)
installUUID, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, *installer.SoftwareInstallerID, fleet.HostSoftwareInstallOptions{
SelfService: false,
ForSetupExperience: true,
})
if err != nil {
return false, ctxerr.Wrap(ctx, err, "queueing setup experience install request")
}
Expand Down Expand Up @@ -207,7 +210,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string
},
}

cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, false)
cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, fleet.HostSoftwareInstallOptions{
SelfService: false,
ForSetupExperience: true,
})
if err != nil {
return false, ctxerr.Wrap(ctx, err, "queueing vpp app installation")
}
Expand All @@ -224,9 +230,12 @@ 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,
// because the script execution request is associated with setup experience,
// it will be enqueued with a higher priority and will run before other
// items in the queue.
SetupExperienceScriptID: script.SetupExperienceScriptID,
}
res, err := svc.ds.NewHostScriptExecutionRequest(ctx, req)
Expand Down
2 changes: 1 addition & 1 deletion ee/server/service/setup_experience_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestSetupExperienceNextStep(t *testing.T) {
return mockListHostsLite, nil
}

ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, selfService bool, policyID *uint) (string, error) {
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string, error) {
requestedInstalls[hostID] = append(requestedInstalls[hostID], softwareInstallerID)
return "install-uuid", nil
}
Expand Down
72 changes: 32 additions & 40 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import (

"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
"github.com/fleetdm/fleet/v4/server/authz"
authz_ctx "github.com/fleetdm/fleet/v4/server/contexts/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
Expand Down Expand Up @@ -1001,17 +1000,19 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
return ctxerr.Wrap(ctx, err, "finding VPP app for title")
}

_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, false)
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
SelfService: false,
})
return err
}

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

return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, selfService, nil)
return svc.InstallVPPAppPostValidation(ctx, host, vppApp, token, opts)
}

func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDevice bool, host *fleet.Host) (string, error) {
Expand Down Expand Up @@ -1057,7 +1058,7 @@ func (svc *Service) GetVPPTokenIfCanInstallVPPApps(ctx context.Context, appleDev
return token, nil
}

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

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

err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, selfService, policyID)
// enqueue the VPP app command to install
cmdUUID := uuid.NewString()
err = svc.ds.InsertHostVPPSoftwareInstall(ctx, host.ID, vppApp.VPPAppID, cmdUUID, eventID, opts)
if err != nil {
return "", ctxerr.Wrapf(ctx, err, "inserting host vpp software install for host with serial %s and app with adamID %s", host.HardwareSerial, vppApp.AdamID)
}
Expand All @@ -1154,7 +1160,9 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
}
}

_, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, false, nil)
_, err := svc.ds.InsertSoftwareInstallRequest(ctx, host.ID, installer.InstallerID, fleet.HostSoftwareInstallOptions{
SelfService: false,
})
return ctxerr.Wrap(ctx, err, "inserting software install request")
}

Expand Down Expand Up @@ -1244,8 +1252,9 @@ func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, sof
}
}

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

var teamID uint
if host.TeamID != nil {
teamID = *host.TeamID
}
// create the script execution request; the host will be notified of the
// script execution request via the orbit config's Notifications mechanism.
request := fleet.HostScriptRequestPayload{
HostID: host.ID,
ScriptContents: string(contents),
ScriptContentID: installer.UninstallScriptContentID,
TeamID: teamID,
}
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil {
request.UserID = &ctxUser.ID
}
scriptResult, err := svc.ds.NewInternalScriptExecutionRequest(ctx, &request)
if err != nil {
return ctxerr.Wrap(ctx, err, "create script execution request")
}

// Update the host software installs table with the uninstall request.
// Pending uninstalls will automatically show up in the UI Host Details -> Activity -> Upcoming tab.
if err = svc.insertSoftwareUninstallRequest(ctx, scriptResult.ExecutionID, host, installer); err != nil {
execID := uuid.NewString()
if err = svc.insertSoftwareUninstallRequest(ctx, execID, host, installer); err != nil {
return err
}

return nil
}

Expand Down Expand Up @@ -1818,7 +1806,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f
}
}

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

Expand Down Expand Up @@ -1852,7 +1842,9 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f
platform := host.FleetPlatform()
mobileAppleDevice := fleet.AppleDevicePlatform(platform) == fleet.IOSPlatform || fleet.AppleDevicePlatform(platform) == fleet.IPadOSPlatform

_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, true)
_, err = svc.installSoftwareFromVPP(ctx, host, vppApp, mobileAppleDevice || fleet.AppleDevicePlatform(platform) == fleet.MacOSPlatform, fleet.HostSoftwareInstallOptions{
SelfService: true,
})
return err
}

Expand Down
8 changes: 1 addition & 7 deletions ee/server/service/software_installers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,14 @@ func TestInstallUninstallAuth(t *testing.T) {
ds.GetHostLastInstallDataFunc = func(ctx context.Context, hostID uint, installerID uint) (*fleet.HostLastInstallData, error) {
return nil, nil
}
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool, policyID *uint) (string,
ds.InsertSoftwareInstallRequestFunc = func(ctx context.Context, hostID uint, softwareInstallerID uint, opts fleet.HostSoftwareInstallOptions) (string,
error,
) {
return "request_id", nil
}
ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) {
return []byte("script"), nil
}
ds.NewInternalScriptExecutionRequestFunc = func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult,
error) {
return &fleet.HostScriptResult{
ExecutionID: "execution_id",
}, nil
}
ds.InsertSoftwareUninstallRequestFunc = func(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
return nil
}
Expand Down
1 change: 1 addition & 0 deletions frontend/__mocks__/activityMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const DEFAULT_ACTIVITY_MOCK: IActivity = {
actor_id: 1,
actor_gravatar: "",
actor_email: "[email protected]",
fleet_initiated: false,
type: ActivityType.EditedAgentOptions,
};

Expand Down
18 changes: 2 additions & 16 deletions frontend/components/ActivityItem/ActivityItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react";
import ReactTooltip from "react-tooltip";
import classnames from "classnames";

import { ActivityType, IActivity, IActivityDetails } from "interfaces/activity";
import { IActivity, IActivityDetails } from "interfaces/activity";
import {
addGravatarUrlToResource,
internationalTimeFormat,
Expand Down Expand Up @@ -108,20 +108,6 @@ const ActivityItem = ({
onCancel();
};

// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in
// the backend. For now, if all these fields are empty, then we assume it was
// Fleet-initiated.
let fleetInitiated = false;
if (
!activity.actor_email &&
!activity.actor_full_name &&
(activity.type === ActivityType.InstalledSoftware ||
activity.type === ActivityType.InstalledAppStoreApp ||
activity.type === ActivityType.RanScript)
) {
fleetInitiated = true;
}

return (
<div className={classNames}>
<div className={`${baseClass}__avatar-wrapper`}>
Expand All @@ -131,7 +117,7 @@ const ActivityItem = ({
user={{ gravatar_url }}
size="small"
hasWhiteBackground
useFleetAvatar={fleetInitiated}
useFleetAvatar={activity.fleet_initiated}
/>
<div className={`${baseClass}__avatar-lower-dash`} />
</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ export interface IActivity {
actor_gravatar: string;
actor_email?: string;
type: ActivityType;
fleet_initiated: boolean;
details?: IActivityDetails;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1278,18 +1278,6 @@ const GlobalActivityItem = ({
}: IActivityItemProps) => {
const hasDetails = ACTIVITIES_WITH_DETAILS.has(activity.type);

// Add the "Fleet" name to the activity if needed.
// TODO: remove/refactor this once we have "fleet-initiated" activities.
if (
!activity.actor_email &&
!activity.actor_full_name &&
(activity.type === ActivityType.InstalledSoftware ||
activity.type === ActivityType.InstalledAppStoreApp ||
activity.type === ActivityType.RanScript)
) {
activity.actor_full_name = "Fleet";
}

const renderActivityPrefix = () => {
const DEFAULT_ACTOR_DISPLAY = <b>{activity.actor_full_name} </b>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

import { ActivityType, IHostPastActivity } from "interfaces/activity";
import { IHostPastActivity } from "interfaces/activity";
import { IHostPastActivitiesResponse } from "services/entities/activities";

// @ts-ignore
Expand Down Expand Up @@ -54,18 +54,6 @@ const PastActivityFeed = ({
<div className={baseClass}>
<div>
{activitiesList.map((activity: IHostPastActivity) => {
// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in
// the backend. For now, if all these fields are empty, then we assume it was
// Fleet-initiated.
if (
!activity.actor_email &&
!activity.actor_full_name &&
(activity.type === ActivityType.InstalledSoftware ||
activity.type === ActivityType.InstalledAppStoreApp ||
activity.type === ActivityType.RanScript)
) {
activity.actor_full_name = "Fleet";
}
const ActivityItemComponent = pastActivityComponentMap[activity.type];
return (
<ActivityItemComponent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";

import { ActivityType, IHostUpcomingActivity } from "interfaces/activity";
import { IHostUpcomingActivity } from "interfaces/activity";
import { IHostUpcomingActivitiesResponse } from "services/entities/activities";

// @ts-ignore
Expand Down Expand Up @@ -55,18 +55,6 @@ const UpcomingActivityFeed = ({
<div className={baseClass}>
<div className={`${baseClass}__feed-list`}>
{activitiesList.map((activity: IHostUpcomingActivity) => {
// TODO: remove this once we have a proper way of handling "Fleet-initiated" activities in
// the backend. For now, if all these fields are empty, then we assume it was
// Fleet-initiated.
if (
!activity.actor_email &&
!activity.actor_full_name &&
(activity.type === ActivityType.InstalledSoftware ||
activity.type === ActivityType.InstalledAppStoreApp ||
activity.type === ActivityType.RanScript)
) {
activity.actor_full_name = "Fleet";
}
const ActivityItemComponent =
upcomingActivityComponentMap[activity.type];
return (
Expand Down
Loading
Loading