package service import ( "context" "errors" "io" "net/http" "path/filepath" "github.com/fleetdm/fleet/v4/server/authz" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" "github.com/fleetdm/fleet/v4/server/fleet" "github.com/fleetdm/fleet/v4/server/ptr" "github.com/go-kit/log/level" ) func (svc *Service) SetSetupExperienceSoftware(ctx context.Context, platform string, teamID uint, titleIDs []uint) error { if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: &teamID}, fleet.ActionWrite); err != nil { return err } var teamName string if teamID == 0 { teamName = "" } else { team, err := svc.ds.Team(ctx, teamID) if err != nil { return ctxerr.Wrap(ctx, err, "load team") } teamName = team.Name } if err := svc.ds.SetSetupExperienceSoftwareTitles(ctx, platform, teamID, titleIDs); err != nil { return ctxerr.Wrap(ctx, err, "setting setup experience titles") } if err := svc.NewActivity( ctx, authz.UserFromContext(ctx), fleet.ActivityEditedSetupExperienceSoftware{ Platform: platform, TeamID: teamID, TeamName: teamName, }, ); err != nil { return ctxerr.Wrap(ctx, err, "create activity for set setup experience software") } return nil } func (svc *Service) ListSetupExperienceSoftware(ctx context.Context, platform string, teamID uint, opts fleet.ListOptions) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) { if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{ TeamID: &teamID, }, fleet.ActionRead); err != nil { return nil, 0, nil, err } titles, count, meta, err := svc.ds.ListSetupExperienceSoftwareTitles(ctx, platform, teamID, opts) if err != nil { return nil, 0, nil, ctxerr.Wrap(ctx, err, "retrieving list of software setup experience titles") } return titles, count, meta, nil } func (svc *Service) GetSetupExperienceScript(ctx context.Context, teamID *uint, withContent bool) (*fleet.Script, []byte, error) { if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: teamID}, fleet.ActionRead); err != nil { return nil, nil, err } script, err := svc.ds.GetSetupExperienceScript(ctx, teamID) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "get setup experience script") } var content []byte if withContent { content, err = svc.ds.GetAnyScriptContents(ctx, script.ScriptContentID) if err != nil { return nil, nil, ctxerr.Wrap(ctx, err, "get setup experience script contents") } } return script, content, nil } func (svc *Service) SetSetupExperienceScript(ctx context.Context, teamID *uint, name string, r io.Reader) error { if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: teamID}, fleet.ActionWrite); err != nil { return err } b, err := io.ReadAll(r) if err != nil { return ctxerr.Wrap(ctx, err, "read setup experience script contents") } script := &fleet.Script{ TeamID: teamID, Name: name, ScriptContents: string(b), } if err := svc.ds.ValidateEmbeddedSecrets(ctx, []string{script.ScriptContents}); err != nil { return fleet.NewInvalidArgumentError("script", err.Error()) } // setup experience is only supported for macOS currently so we need to override the file // extension check in the general script validation if filepath.Ext(script.Name) != ".sh" { return fleet.NewInvalidArgumentError("script", "File type not supported. Only .sh file type is allowed.") } // now we can do our normal script validation if err := script.ValidateNewScript(); err != nil { return fleet.NewInvalidArgumentError("script", err.Error()) } if err := svc.ds.SetSetupExperienceScript(ctx, script); err != nil { var ( existsErr fleet.AlreadyExistsError fkErr fleet.ForeignKeyError ) if errors.As(err, &existsErr) { err = fleet.NewInvalidArgumentError("script", err.Error()).WithStatus(http.StatusConflict) // TODO: confirm error message with product/frontend } else if errors.As(err, &fkErr) { err = fleet.NewInvalidArgumentError("team_id", "The team does not exist.").WithStatus(http.StatusNotFound) } return ctxerr.Wrap(ctx, err, "create setup experience script") } // NOTE: there is no activity specified for set setup experience script return nil } func (svc *Service) DeleteSetupExperienceScript(ctx context.Context, teamID *uint) error { if err := svc.authz.Authorize(ctx, &fleet.Script{TeamID: teamID}, fleet.ActionWrite); err != nil { return err } if err := svc.ds.DeleteSetupExperienceScript(ctx, teamID); err != nil { return ctxerr.Wrap(ctx, err, "delete setup experience script") } // NOTE: there is no activity specified for delete setup experience script return nil } func (svc *Service) SetupExperienceNextStep(ctx context.Context, host *fleet.Host) (bool, error) { hostUUID, err := fleet.HostUUIDForSetupExperience(host) if err != nil { return false, ctxerr.Wrap(ctx, err, "failed to get host's UUID for the setup experience") } statuses, err := svc.ds.ListSetupExperienceResultsByHostUUID(ctx, hostUUID) if err != nil { return false, ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step") } var installersPending, appsPending, scriptsPending []*fleet.SetupExperienceStatusResult var installersRunning, appsRunning, scriptsRunning int for _, status := range statuses { if err := status.IsValid(); err != nil { return false, ctxerr.Wrap(ctx, err, "invalid row") } switch { case status.SoftwareInstallerID != nil: switch status.Status { case fleet.SetupExperienceStatusPending: installersPending = append(installersPending, status) case fleet.SetupExperienceStatusRunning: installersRunning++ } case status.VPPAppTeamID != nil: switch status.Status { case fleet.SetupExperienceStatusPending: appsPending = append(appsPending, status) case fleet.SetupExperienceStatusRunning: appsRunning++ } case status.SetupExperienceScriptID != nil: switch status.Status { case fleet.SetupExperienceStatusPending: scriptsPending = append(scriptsPending, status) case fleet.SetupExperienceStatusRunning: scriptsRunning++ } } } switch { case len(installersPending) > 0: // enqueue installers for _, installer := range installersPending { 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") } installer.HostSoftwareInstallsExecutionID = &installUUID installer.Status = fleet.SetupExperienceStatusRunning if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, installer); err != nil { return false, ctxerr.Wrap(ctx, err, "updating setup experience result with install uuid") } } case installersRunning == 0 && len(appsPending) > 0: // enqueue vpp apps for _, app := range appsPending { vppAppID, err := app.VPPAppID() if err != nil { return false, ctxerr.Wrap(ctx, err, "constructing vpp app details for installation") } if app.SoftwareTitleID == nil { return false, ctxerr.Errorf(ctx, "setup experience software title id missing from vpp app install request: %d", app.ID) } vppApp := &fleet.VPPApp{ TitleID: *app.SoftwareTitleID, VPPAppTeam: fleet.VPPAppTeam{ VPPAppID: *vppAppID, }, } cmdUUID, err := svc.installSoftwareFromVPP(ctx, host, vppApp, true, fleet.HostSoftwareInstallOptions{ SelfService: false, ForSetupExperience: true, }) app.NanoCommandUUID = &cmdUUID app.Status = fleet.SetupExperienceStatusRunning if err != nil { // if we get an error (e.g. no available licenses) while attempting to enqueue the // install, then we should immediately go to an error state so setup experience // isn't blocked. level.Warn(svc.logger).Log("msg", "got an error when attempting to enqueue VPP app install", "err", err, "adam_id", app.VPPAppAdamID) app.Status = fleet.SetupExperienceStatusFailure app.Error = ptr.String(err.Error()) } if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, app); err != nil { return false, ctxerr.Wrap(ctx, err, "updating setup experience with vpp install command uuid") } } case installersRunning == 0 && appsRunning == 0 && len(scriptsPending) > 0: // enqueue scripts for _, script := range scriptsPending { if script.ScriptContentID == nil { 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, // 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) if err != nil { return false, ctxerr.Wrap(ctx, err, "queueing setup experience script execution request") } script.ScriptExecutionID = &res.ExecutionID script.Status = fleet.SetupExperienceStatusRunning if err := svc.ds.UpdateSetupExperienceStatusResult(ctx, script); err != nil { return false, ctxerr.Wrap(ctx, err, "updating setup experience script execution id") } } case installersRunning == 0 && appsRunning == 0 && scriptsRunning == 0: // finished return true, nil } return false, nil }