mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Uninstall packages (#21892)
#20320 # Demo video(s) - API demo: https://www.loom.com/share/037c82cbde9743cfa42778eb04612482 # Checklist for submitter - [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 BE tests - [ ] Added/updated FE tests - [ ] 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 BE QA for all new/changed functionality - [ ] Manual end-to-end QA for all new/changed functionality
This commit is contained in:
commit
8e5d056198
91 changed files with 3143 additions and 759 deletions
1
changes/20320-uninstall-packages
Normal file
1
changes/20320-uninstall-packages
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Implement the ability to use Fleet to uninstall packages from hosts.
|
||||
|
|
@ -25,3 +25,4 @@ flag_management:
|
|||
|
||||
ignore:
|
||||
- "server/mock"
|
||||
- "server/fleet/activities.go" # mostly contains code for documentation -- not interesting for tests
|
||||
|
|
|
|||
|
|
@ -1170,6 +1170,29 @@ This activity contains the following fields:
|
|||
}
|
||||
```
|
||||
|
||||
## uninstalled_software
|
||||
|
||||
Generated when a software is uninstalled on a host.
|
||||
|
||||
This activity contains the following fields:
|
||||
- "host_id": ID of the host.
|
||||
- "host_display_name": Display name of the host.
|
||||
- "software_title": Name of the software.
|
||||
- "script_execution_id": ID of the software uninstall script.
|
||||
- "status": Status of the software uninstallation.
|
||||
|
||||
#### Example
|
||||
|
||||
```json
|
||||
{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"software_title": "Falcon.app",
|
||||
"script_execution_id": "ece8d99d-4313-446a-9af2-e152cd1bad1e",
|
||||
"status": "uninstalled"
|
||||
}
|
||||
```
|
||||
|
||||
## added_software
|
||||
|
||||
Generated when a software installer is uploaded to Fleet.
|
||||
|
|
|
|||
|
|
@ -12,9 +12,12 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
hostctx "github.com/fleetdm/fleet/v4/server/contexts/host"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
|
|
@ -44,6 +47,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
// shebang when the file is directly executed.
|
||||
payload.InstallScript = file.Dos2UnixNewlines(payload.InstallScript)
|
||||
payload.PostInstallScript = file.Dos2UnixNewlines(payload.PostInstallScript)
|
||||
payload.UninstallScript = file.Dos2UnixNewlines(payload.UninstallScript)
|
||||
|
||||
if _, err := svc.addMetadataToSoftwarePayload(ctx, payload); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "adding metadata to payload")
|
||||
|
|
@ -56,6 +60,9 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
// TODO: basic validation of install and post-install script (e.g., supported interpreters)?
|
||||
// TODO: any validation of pre-install query?
|
||||
|
||||
// Update $PACKAGE_ID in uninstall script
|
||||
preProcessUninstallScript(payload)
|
||||
|
||||
installerID, err := svc.ds.MatchOrCreateSoftwareInstaller(ctx, payload)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "matching or creating software installer")
|
||||
|
|
@ -87,6 +94,28 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
return nil
|
||||
}
|
||||
|
||||
var packageIDRegex = regexp.MustCompile(`((("\$PACKAGE_ID")|(\$PACKAGE_ID))(?P<suffix>\W|$))|(("\${PACKAGE_ID}")|(\${PACKAGE_ID}))`)
|
||||
|
||||
func preProcessUninstallScript(payload *fleet.UploadSoftwareInstallerPayload) {
|
||||
// We assume that we already validated that payload.PackageIDs is not empty.
|
||||
// Replace $PACKAGE_ID in the uninstall script with the package ID(s).
|
||||
var packageID string
|
||||
switch payload.Extension {
|
||||
case "pkg":
|
||||
var sb strings.Builder
|
||||
_, _ = sb.WriteString("(\n")
|
||||
for _, pkgID := range payload.PackageIDs {
|
||||
_, _ = sb.WriteString(fmt.Sprintf(" \"%s\"\n", pkgID))
|
||||
}
|
||||
_, _ = sb.WriteString(")") // no ending newline
|
||||
packageID = sb.String()
|
||||
default:
|
||||
packageID = fmt.Sprintf("\"%s\"", payload.PackageIDs[0])
|
||||
}
|
||||
|
||||
payload.UninstallScript = packageIDRegex.ReplaceAllString(payload.UninstallScript, fmt.Sprintf("%s${suffix}", packageID))
|
||||
}
|
||||
|
||||
func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, teamID *uint) error {
|
||||
if teamID == nil {
|
||||
return fleet.NewInvalidArgumentError("team_id", "is required")
|
||||
|
|
@ -389,11 +418,12 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
|
|||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "getting last install data for host %d and installer %d", host.ID, installer.InstallerID)
|
||||
}
|
||||
if lastInstallRequest != nil && lastInstallRequest.Status != nil && *lastInstallRequest.Status == fleet.SoftwareInstallerPending {
|
||||
if lastInstallRequest != nil && lastInstallRequest.Status != nil &&
|
||||
(*lastInstallRequest.Status == fleet.SoftwareInstallPending || *lastInstallRequest.Status == fleet.SoftwareUninstallPending) {
|
||||
return &fleet.BadRequestError{
|
||||
Message: "Couldn't install software. Host has a pending install request.",
|
||||
Message: "Couldn't install software. Host has a pending install/uninstall request.",
|
||||
InternalErr: ctxerr.WrapWithData(
|
||||
ctx, err, "host already has a pending install for this installer",
|
||||
ctx, err, "host already has a pending install/uninstall for this installer",
|
||||
map[string]any{
|
||||
"host_id": host.ID,
|
||||
"software_installer_id": installer.InstallerID,
|
||||
|
|
@ -568,6 +598,152 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
|
|||
return ctxerr.Wrap(ctx, err, "inserting software install request")
|
||||
}
|
||||
|
||||
func (svc *Service) UninstallSoftwareTitle(ctx context.Context, hostID uint, softwareTitleID uint) error {
|
||||
// First check if scripts are disabled globally. If so, no need for further processing.
|
||||
cfg, err := svc.ds.AppConfig(ctx)
|
||||
if err != nil {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
if cfg.ServerSettings.ScriptsDisabled {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return fleet.NewUserMessageError(errors.New(fleet.RunScriptScriptsDisabledGloballyErrMsg), http.StatusForbidden)
|
||||
}
|
||||
|
||||
// we need to use ds.Host because ds.HostLite doesn't return the orbit node key
|
||||
host, err := svc.ds.Host(ctx, hostID)
|
||||
if err != nil {
|
||||
// if error is because the host does not exist, check first if the user
|
||||
// had access to install/uninstall software (to prevent leaking valid host ids).
|
||||
if fleet.IsNotFound(err) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return ctxerr.Wrap(ctx, err, "get host")
|
||||
}
|
||||
|
||||
if host.OrbitNodeKey == nil || *host.OrbitNodeKey == "" {
|
||||
// fleetd is required to install software so if the host is enrolled via plain osquery we return an error
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return fleet.NewUserMessageError(errors.New("host does not have fleetd installed"), http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
// If scripts are disabled (according to the last detail query), we return an error.
|
||||
// host.ScriptsEnabled may be nil for older orbit versions.
|
||||
if host.ScriptsEnabled != nil && !*host.ScriptsEnabled {
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return fleet.NewUserMessageError(errors.New(fleet.RunScriptsOrbitDisabledErrMsg), http.StatusUnprocessableEntity)
|
||||
}
|
||||
|
||||
// authorize with the host's team
|
||||
if err := svc.authz.Authorize(ctx, &fleet.HostSoftwareInstallerResultAuthz{HostTeamID: host.TeamID}, fleet.ActionWrite); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installer, err := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, host.TeamID, softwareTitleID, false)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return &fleet.BadRequestError{
|
||||
Message: "Couldn't uninstall software. Software title is not available for uninstall. Please add software package to install/uninstall.",
|
||||
InternalErr: ctxerr.WrapWithData(
|
||||
ctx, err, "couldn't find an installer for software title",
|
||||
map[string]any{"host_id": host.ID, "team_id": host.TeamID, "title_id": softwareTitleID},
|
||||
),
|
||||
}
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "finding software installer for title")
|
||||
}
|
||||
|
||||
lastInstallRequest, err := svc.ds.GetHostLastInstallData(ctx, host.ID, installer.InstallerID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "getting last install data for host %d and installer %d", host.ID, installer.InstallerID)
|
||||
}
|
||||
if lastInstallRequest != nil && lastInstallRequest.Status != nil &&
|
||||
(*lastInstallRequest.Status == fleet.SoftwareInstallPending || *lastInstallRequest.Status == fleet.SoftwareUninstallPending) {
|
||||
return &fleet.BadRequestError{
|
||||
Message: "Couldn't uninstall software. Host has a pending install/uninstall request.",
|
||||
InternalErr: ctxerr.WrapWithData(
|
||||
ctx, err, "host already has a pending install/uninstall for this installer",
|
||||
map[string]any{
|
||||
"host_id": host.ID,
|
||||
"software_installer_id": installer.InstallerID,
|
||||
"team_id": host.TeamID,
|
||||
"title_id": softwareTitleID,
|
||||
"status": *lastInstallRequest.Status,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate platform
|
||||
ext := filepath.Ext(installer.Name)
|
||||
requiredPlatform := packageExtensionToPlatform(ext)
|
||||
if requiredPlatform == "" {
|
||||
// this should never happen
|
||||
return ctxerr.Errorf(ctx, "software installer has unsupported type %s", ext)
|
||||
}
|
||||
|
||||
if host.FleetPlatform() != requiredPlatform {
|
||||
return &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("Package (%s) can be uninstalled only on %s hosts.", ext, requiredPlatform),
|
||||
InternalErr: ctxerr.NewWithData(
|
||||
ctx, "invalid host platform for requested uninstall",
|
||||
map[string]any{"host_id": host.ID, "team_id": host.TeamID, "title_id": installer.TitleID},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Get the uninstall script and use the standard script infrastructure to run it.
|
||||
contents, err := svc.ds.GetAnyScriptContents(ctx, installer.UninstallScriptContentID)
|
||||
if err != nil {
|
||||
if fleet.IsNotFound(err) {
|
||||
return ctxerr.Wrap(ctx,
|
||||
fleet.NewInvalidArgumentError("software_title_id", `No uninstall script exists for the provided "software_title_id".`).
|
||||
WithStatus(http.StatusNotFound), "getting uninstall script contents")
|
||||
}
|
||||
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.NewHostScriptExecutionRequest(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 {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) insertSoftwareUninstallRequest(ctx context.Context, executionID string, host *fleet.Host,
|
||||
installer *fleet.SoftwareInstaller) error {
|
||||
if err := svc.ds.InsertSoftwareUninstallRequest(ctx, executionID, host.ID, installer.InstallerID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "inserting software uninstall request")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetSoftwareInstallResults(ctx context.Context, resultUUID string) (*fleet.HostSoftwareInstallerResult, error) {
|
||||
// Basic auth check
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Host{}, fleet.ActionList); err != nil {
|
||||
|
|
@ -656,6 +832,13 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
}
|
||||
}
|
||||
|
||||
if len(meta.PackageIDs) == 0 {
|
||||
return "", &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("Couldn't add. Fleet couldn't read the package IDs, product code, or name from %s.", payload.Filename),
|
||||
InternalErr: ctxerr.New(ctx, "extracting package IDs from installer metadata"),
|
||||
}
|
||||
}
|
||||
|
||||
payload.Title = meta.Name
|
||||
if payload.Title == "" {
|
||||
// use the filename if no title from metadata
|
||||
|
|
@ -664,6 +847,8 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
payload.Version = meta.Version
|
||||
payload.StorageID = hex.EncodeToString(meta.SHASum)
|
||||
payload.BundleIdentifier = meta.BundleIdentifier
|
||||
payload.PackageIDs = meta.PackageIDs
|
||||
payload.Extension = meta.Extension
|
||||
|
||||
// reset the reader (it was consumed to extract metadata)
|
||||
if _, err := payload.InstallerFile.Seek(0, 0); err != nil {
|
||||
|
|
@ -674,6 +859,10 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
payload.InstallScript = file.GetInstallScript(meta.Extension)
|
||||
}
|
||||
|
||||
if payload.UninstallScript == "" {
|
||||
payload.UninstallScript = file.GetUninstallScript(meta.Extension)
|
||||
}
|
||||
|
||||
source, err := fleet.SofwareInstallerSourceFromExtensionAndName(meta.Extension, meta.Name)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "determining source from extension and name")
|
||||
|
|
|
|||
223
ee/server/service/software_installers_test.go
Normal file
223
ee/server/service/software_installers_test.go
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/authz"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestPreProcessUninstallScript(t *testing.T) {
|
||||
t.Parallel()
|
||||
var input = `
|
||||
blah$PACKAGE_IDS
|
||||
pkgids=$PACKAGE_ID
|
||||
they are $PACKAGE_ID, right $MY_SECRET?
|
||||
quotes for "$PACKAGE_ID"
|
||||
blah${PACKAGE_ID}withConcat
|
||||
quotes and braces for "${PACKAGE_ID}"
|
||||
${PACKAGE_ID}`
|
||||
|
||||
payload := fleet.UploadSoftwareInstallerPayload{
|
||||
Extension: "exe",
|
||||
UninstallScript: input,
|
||||
PackageIDs: []string{"com.foo"},
|
||||
}
|
||||
|
||||
preProcessUninstallScript(&payload)
|
||||
expected := `
|
||||
blah$PACKAGE_IDS
|
||||
pkgids="com.foo"
|
||||
they are "com.foo", right $MY_SECRET?
|
||||
quotes for "com.foo"
|
||||
blah"com.foo"withConcat
|
||||
quotes and braces for "com.foo"
|
||||
"com.foo"`
|
||||
assert.Equal(t, expected, payload.UninstallScript)
|
||||
|
||||
payload = fleet.UploadSoftwareInstallerPayload{
|
||||
Extension: "pkg",
|
||||
UninstallScript: input,
|
||||
PackageIDs: []string{"com.foo", "com.bar"},
|
||||
}
|
||||
preProcessUninstallScript(&payload)
|
||||
expected = `
|
||||
blah$PACKAGE_IDS
|
||||
pkgids=(
|
||||
"com.foo"
|
||||
"com.bar"
|
||||
)
|
||||
they are (
|
||||
"com.foo"
|
||||
"com.bar"
|
||||
), right $MY_SECRET?
|
||||
quotes for (
|
||||
"com.foo"
|
||||
"com.bar"
|
||||
)
|
||||
blah(
|
||||
"com.foo"
|
||||
"com.bar"
|
||||
)withConcat
|
||||
quotes and braces for (
|
||||
"com.foo"
|
||||
"com.bar"
|
||||
)
|
||||
(
|
||||
"com.foo"
|
||||
"com.bar"
|
||||
)`
|
||||
assert.Equal(t, expected, payload.UninstallScript)
|
||||
|
||||
}
|
||||
|
||||
func TestInstallUninstallAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return &fleet.Host{
|
||||
OrbitNodeKey: ptr.String("orbit_key"),
|
||||
Platform: "darwin",
|
||||
TeamID: ptr.Uint(1),
|
||||
}, nil
|
||||
}
|
||||
ds.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint,
|
||||
withScriptContents bool) (*fleet.SoftwareInstaller, error) {
|
||||
return &fleet.SoftwareInstaller{
|
||||
Name: "installer.pkg",
|
||||
Platform: "darwin",
|
||||
TeamID: ptr.Uint(1),
|
||||
}, nil
|
||||
}
|
||||
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) (string,
|
||||
error) {
|
||||
return "request_id", nil
|
||||
}
|
||||
ds.GetAnyScriptContentsFunc = func(ctx context.Context, id uint) ([]byte, error) {
|
||||
return []byte("script"), nil
|
||||
}
|
||||
ds.NewHostScriptExecutionRequestFunc = 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
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
user *fleet.User
|
||||
shouldFail bool
|
||||
}{
|
||||
{
|
||||
"global admin",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global maintainer",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleMaintainer)},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"global observer",
|
||||
&fleet.User{GlobalRole: ptr.String(fleet.RoleObserver)},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"team admin",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleAdmin}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"team maintainer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleMaintainer}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"team observer",
|
||||
&fleet.User{Teams: []fleet.UserTeam{{Team: fleet.Team{ID: 1}, Role: fleet.RoleObserver}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: tt.user})
|
||||
checkAuthErr(t, tt.shouldFail, svc.InstallSoftwareTitle(ctx, 1, 10))
|
||||
checkAuthErr(t, tt.shouldFail, svc.UninstallSoftwareTitle(ctx, 1, 10))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestUninstallSoftwareTitle is mostly tested in enterprise integration test. This test hits a few edge cases.
|
||||
func TestUninstallSoftwareTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
svc := newTestService(t, ds)
|
||||
|
||||
host := &fleet.Host{
|
||||
OrbitNodeKey: ptr.String("orbit_key"),
|
||||
Platform: "darwin",
|
||||
TeamID: ptr.Uint(1),
|
||||
}
|
||||
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
return host, nil
|
||||
}
|
||||
|
||||
// Scripts disabled
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
ScriptsDisabled: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptScriptsDisabledGloballyErrMsg)
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{}, nil
|
||||
}
|
||||
|
||||
// Host scripts disabled
|
||||
host.ScriptsEnabled = ptr.Bool(false)
|
||||
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptsOrbitDisabledErrMsg)
|
||||
|
||||
}
|
||||
|
||||
func checkAuthErr(t *testing.T, shouldFail bool, err error) {
|
||||
t.Helper()
|
||||
if shouldFail {
|
||||
require.Error(t, err)
|
||||
var forbiddenError *authz.Forbidden
|
||||
require.ErrorAs(t, err, &forbiddenError)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newTestService(t *testing.T, ds fleet.Datastore) *Service {
|
||||
t.Helper()
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
require.NoError(t, err)
|
||||
svc := &Service{
|
||||
authz: authorizer,
|
||||
ds: ds,
|
||||
}
|
||||
return svc
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ const DEFAULT_SCRIPT_RESULT_MOCK: IScriptResultResponse = {
|
|||
runtime: 0,
|
||||
host_timeout: false,
|
||||
script_id: 1,
|
||||
created_at: "2020-01-01T00:00:00.000Z",
|
||||
};
|
||||
|
||||
export const createMockScriptResult = (
|
||||
|
|
|
|||
|
|
@ -204,8 +204,10 @@ const DEFAULT_SOFTWARE_PACKAGE_MOCK: ISoftwarePackage = {
|
|||
icon_url: null,
|
||||
status: {
|
||||
installed: 1,
|
||||
pending: 2,
|
||||
failed: 3,
|
||||
pending_install: 2,
|
||||
failed_install: 1,
|
||||
pending_uninstall: 1,
|
||||
failed_uninstall: 1,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
|
||||
import { IActivityDetails } from "interfaces/activity";
|
||||
import {
|
||||
|
|
@ -29,7 +30,14 @@ export type IPackageInstallDetails = Pick<
|
|||
>;
|
||||
|
||||
const StatusMessage = ({
|
||||
result: { host_display_name, software_package, software_title, status },
|
||||
result: {
|
||||
host_display_name,
|
||||
software_package,
|
||||
software_title,
|
||||
status,
|
||||
updated_at,
|
||||
created_at,
|
||||
},
|
||||
}: {
|
||||
result: ISoftwareInstallResult;
|
||||
}) => {
|
||||
|
|
@ -38,13 +46,24 @@ const StatusMessage = ({
|
|||
) : (
|
||||
"the host"
|
||||
);
|
||||
|
||||
const timeStamp = updated_at || created_at;
|
||||
const displayTimeStamp = ["failed_install", "installed"].includes(
|
||||
status || ""
|
||||
)
|
||||
? ` (${formatDistanceToNow(new Date(timeStamp), {
|
||||
includeSeconds: true,
|
||||
addSuffix: true,
|
||||
})})`
|
||||
: "";
|
||||
return (
|
||||
<div className={`${baseClass}__status-message`}>
|
||||
<Icon name={INSTALL_DETAILS_STATUS_ICONS[status]} />
|
||||
<span>
|
||||
Fleet {getInstallDetailsStatusPredicate(status)} <b>{software_title}</b>{" "}
|
||||
({software_package}) on {formattedHost}
|
||||
{status === "pending" ? " when it comes online" : ""}.
|
||||
{status === "pending_install" ? " when it comes online" : ""}
|
||||
{displayTimeStamp}.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -104,7 +123,7 @@ export const SoftwareInstallDetails = ({
|
|||
result.host_display_name ? result : { ...result, host_display_name } // prefer result.host_display_name (it may be empty if the host was deleted) otherwise default to whatever we received via props
|
||||
}
|
||||
/>
|
||||
{result.status !== "pending" && (
|
||||
{result.status !== "pending_install" && (
|
||||
<>
|
||||
{result.pre_install_query_output && (
|
||||
<Output displayKey="pre_install_query_output" result={result} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
import Button from "components/buttons/Button";
|
||||
import DataError from "components/DataError";
|
||||
import Icon from "components/Icon";
|
||||
import Modal from "components/Modal";
|
||||
import Spinner from "components/Spinner";
|
||||
import Textarea from "components/Textarea";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { IActivityDetails } from "interfaces/activity";
|
||||
import { isPendingStatus, SoftwareInstallStatus } from "interfaces/software";
|
||||
import React from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import scriptsAPI, { IScriptResultResponse } from "services/entities/scripts";
|
||||
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
|
||||
import {
|
||||
getInstallDetailsStatusPredicate,
|
||||
INSTALL_DETAILS_STATUS_ICONS,
|
||||
} from "../constants";
|
||||
|
||||
const baseClass = "software-uninstall-details-modal";
|
||||
|
||||
type ISoftwareUninstallDetails = Pick<
|
||||
IActivityDetails,
|
||||
"script_execution_id" | "host_display_name" | "software_title" | "status"
|
||||
>;
|
||||
// TODO - rely on activity created_at for timestamp? what else?
|
||||
|
||||
interface IUninstallStatusMessage {
|
||||
host_display_name: string;
|
||||
// TODO - improve status typing
|
||||
status: string;
|
||||
software_title: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
const StatusMessage = ({
|
||||
host_display_name,
|
||||
status,
|
||||
software_title,
|
||||
timestamp,
|
||||
}: IUninstallStatusMessage) => {
|
||||
const formattedHost = host_display_name ? (
|
||||
<b>{host_display_name}</b>
|
||||
) : (
|
||||
"the host"
|
||||
);
|
||||
|
||||
const isPending = isPendingStatus(status);
|
||||
const displayTimeStamp =
|
||||
!isPending && timestamp
|
||||
? ` (${formatDistanceToNow(new Date(timestamp), {
|
||||
includeSeconds: true,
|
||||
addSuffix: true,
|
||||
})})`
|
||||
: "";
|
||||
return (
|
||||
<div className={`${baseClass}__status-message`}>
|
||||
<Icon
|
||||
name={INSTALL_DETAILS_STATUS_ICONS[status as SoftwareInstallStatus]}
|
||||
/>
|
||||
<span>
|
||||
Fleet {getInstallDetailsStatusPredicate(status)} <b>{software_title}</b>{" "}
|
||||
from {formattedHost}
|
||||
{isPending ? " when it comes online" : ""}
|
||||
{displayTimeStamp}.
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const SoftwareUninstallDetailsModal = ({
|
||||
details,
|
||||
onCancel,
|
||||
}: {
|
||||
details: ISoftwareUninstallDetails;
|
||||
onCancel: () => void;
|
||||
}) => {
|
||||
const SoftwareUninstallDetails = ({
|
||||
script_execution_id = "",
|
||||
host_display_name = "",
|
||||
software_title = "",
|
||||
status = "",
|
||||
}: ISoftwareUninstallDetails) => {
|
||||
const {
|
||||
data: scriptResult,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery<IScriptResultResponse>(
|
||||
["uninstallResult", details.script_execution_id],
|
||||
() => {
|
||||
return scriptsAPI.getScriptResult(script_execution_id);
|
||||
},
|
||||
{ ...DEFAULT_USE_QUERY_OPTIONS }
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return <Spinner />;
|
||||
} else if (isError) {
|
||||
return <DataError description="Close this modal and try again." />;
|
||||
} else if (!scriptResult) {
|
||||
// FIXME: Find a better solution for this.
|
||||
return <DataError description="No data returned." />;
|
||||
}
|
||||
|
||||
status = status === "failed" ? "failed_uninstall" : status;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatusMessage
|
||||
host_display_name={host_display_name}
|
||||
status={status}
|
||||
software_title={software_title}
|
||||
timestamp={scriptResult.created_at}
|
||||
/>
|
||||
{!isPendingStatus(status) && scriptResult && (
|
||||
<>
|
||||
<div className={`${baseClass}__script-output`}>
|
||||
Uninstall script content:
|
||||
<Textarea className={`${baseClass}__output-textarea`}>
|
||||
{scriptResult.script_contents}
|
||||
</Textarea>
|
||||
</div>
|
||||
|
||||
<div className={`${baseClass}__script-output`}>
|
||||
Uninstall script output:
|
||||
<Textarea className={`${baseClass}__output-textarea`}>
|
||||
{scriptResult.output}
|
||||
</Textarea>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Uninstall details"
|
||||
onExit={onCancel}
|
||||
onEnter={onCancel}
|
||||
width="large"
|
||||
className={baseClass}
|
||||
>
|
||||
<>
|
||||
<div className={`${baseClass}__modal-content`}>
|
||||
<SoftwareUninstallDetails {...details} />
|
||||
</div>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button onClick={onCancel} variant="brand">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SoftwareUninstallDetailsModal;
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
.software-uninstall-details-modal {
|
||||
&__modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
}
|
||||
&__status-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: $pad-small;
|
||||
margin: 0;
|
||||
.icon {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
&__script-output {
|
||||
.textarea {
|
||||
margin-top: $pad-medium;
|
||||
overflow-wrap: break-word;
|
||||
font-family: "SourceCodePro", $monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default } from "./SoftwareUninstallDetailsModal";
|
||||
|
|
@ -5,30 +5,36 @@ export const INSTALL_DETAILS_STATUS_ICONS: Record<
|
|||
SoftwareInstallStatus,
|
||||
IconNames
|
||||
> = {
|
||||
pending: "pending-outline",
|
||||
pending_install: "pending-outline",
|
||||
installed: "success-outline",
|
||||
failed: "error-outline",
|
||||
uninstalled: "success-outline",
|
||||
failed_install: "error-outline",
|
||||
pending_uninstall: "pending-outline",
|
||||
failed_uninstall: "error-outline",
|
||||
} as const;
|
||||
|
||||
const INSTALL_DETAILS_STATUS_PREDICATES: Record<
|
||||
SoftwareInstallStatus,
|
||||
string
|
||||
> = {
|
||||
pending: "is installing or will install",
|
||||
pending_install: "is installing or will install",
|
||||
installed: "installed",
|
||||
failed: "failed to install",
|
||||
uninstalled: "uninstalled",
|
||||
failed_install: "failed to install",
|
||||
pending_uninstall: "is uninstalling or will uninstall",
|
||||
failed_uninstall: "failed to uninstall",
|
||||
} as const;
|
||||
|
||||
export const getInstallDetailsStatusPredicate = (
|
||||
status: string | undefined
|
||||
) => {
|
||||
if (!status) {
|
||||
return INSTALL_DETAILS_STATUS_PREDICATES.pending;
|
||||
return INSTALL_DETAILS_STATUS_PREDICATES.pending_install;
|
||||
}
|
||||
return (
|
||||
INSTALL_DETAILS_STATUS_PREDICATES[
|
||||
status.toLowerCase() as SoftwareInstallStatus
|
||||
] || INSTALL_DETAILS_STATUS_PREDICATES.pending
|
||||
] || INSTALL_DETAILS_STATUS_PREDICATES.pending_install
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface IRevealButtonProps {
|
|||
autofocus?: boolean;
|
||||
disabled?: boolean;
|
||||
tooltipContent?: React.ReactNode;
|
||||
disabledTooltipContent?: React.ReactNode;
|
||||
onClick?:
|
||||
| ((value?: any) => void)
|
||||
| ((evt: React.MouseEvent<HTMLButtonElement>) => void);
|
||||
|
|
@ -29,6 +30,7 @@ const RevealButton = ({
|
|||
autofocus,
|
||||
disabled,
|
||||
tooltipContent,
|
||||
disabledTooltipContent,
|
||||
onClick,
|
||||
}: IRevealButtonProps): JSX.Element => {
|
||||
const classNames = classnames(baseClass, className);
|
||||
|
|
@ -36,11 +38,12 @@ const RevealButton = ({
|
|||
const buttonContent = () => {
|
||||
const text = isShowing ? hideText : showText;
|
||||
|
||||
const buttonText = tooltipContent ? (
|
||||
<TooltipWrapper tipContent={tooltipContent}>{text}</TooltipWrapper>
|
||||
) : (
|
||||
text
|
||||
);
|
||||
const buttonText =
|
||||
tooltipContent && !disabled ? (
|
||||
<TooltipWrapper tipContent={tooltipContent}>{text}</TooltipWrapper>
|
||||
) : (
|
||||
text
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -61,7 +64,7 @@ const RevealButton = ({
|
|||
);
|
||||
};
|
||||
|
||||
return (
|
||||
const button = (
|
||||
<Button
|
||||
variant="text-icon"
|
||||
className={classNames}
|
||||
|
|
@ -72,6 +75,22 @@ const RevealButton = ({
|
|||
{buttonContent()}
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (disabled && disabledTooltipContent) {
|
||||
// wrap the tooltip around the Button so it works while disabled
|
||||
return (
|
||||
<TooltipWrapper
|
||||
tipContent={disabledTooltipContent}
|
||||
showArrow
|
||||
underline={false}
|
||||
position="right"
|
||||
tipOffset={12}
|
||||
>
|
||||
{button}
|
||||
</TooltipWrapper>
|
||||
);
|
||||
}
|
||||
return button;
|
||||
};
|
||||
|
||||
export default RevealButton;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ export enum ActivityType {
|
|||
AddedSoftware = "added_software",
|
||||
DeletedSoftware = "deleted_software",
|
||||
InstalledSoftware = "installed_software",
|
||||
UninstalledSoftware = "uninstalled_software",
|
||||
EnabledVpp = "enabled_vpp",
|
||||
DisabledVpp = "disabled_vpp",
|
||||
AddedAppStoreApp = "added_app_store_app",
|
||||
|
|
@ -93,12 +94,14 @@ export type IHostPastActivityType =
|
|||
| ActivityType.LockedHost
|
||||
| ActivityType.UnlockedHost
|
||||
| ActivityType.InstalledSoftware
|
||||
| ActivityType.UninstalledSoftware
|
||||
| ActivityType.InstalledAppStoreApp;
|
||||
|
||||
// This is a subset of ActivityType that are shown only for the host upcoming activities
|
||||
export type IHostUpcomingActivityType =
|
||||
| ActivityType.RanScript
|
||||
| ActivityType.InstalledSoftware
|
||||
| ActivityType.UninstalledSoftware
|
||||
| ActivityType.InstalledAppStoreApp;
|
||||
|
||||
export interface IActivity {
|
||||
|
|
|
|||
22
frontend/interfaces/package_type.ts
Normal file
22
frontend/interfaces/package_type.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const unixPackageTypes = ["pkg", "deb"] as const;
|
||||
const windowsPackageTypes = ["msi", "exe"] as const;
|
||||
export const packageTypes = [
|
||||
...unixPackageTypes,
|
||||
...windowsPackageTypes,
|
||||
] as const;
|
||||
|
||||
export type WindowsPackageType = typeof windowsPackageTypes[number];
|
||||
export type UnixPackageType = typeof unixPackageTypes[number];
|
||||
export type PackageType = WindowsPackageType | UnixPackageType;
|
||||
|
||||
export const isWindowsPackageType = (s: any): s is WindowsPackageType => {
|
||||
return windowsPackageTypes.includes(s);
|
||||
};
|
||||
|
||||
export const isUnixPackageType = (s: any): s is UnixPackageType => {
|
||||
return unixPackageTypes.includes(s);
|
||||
};
|
||||
|
||||
export const isPackageType = (s: any): s is PackageType => {
|
||||
return packageTypes.includes(s);
|
||||
};
|
||||
|
|
@ -66,8 +66,10 @@ export interface ISoftwarePackage {
|
|||
icon_url: string | null;
|
||||
status: {
|
||||
installed: number;
|
||||
pending: number;
|
||||
failed: number;
|
||||
pending_install: number;
|
||||
failed_install: number;
|
||||
pending_uninstall: number;
|
||||
failed_uninstall: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -194,10 +196,19 @@ export const formatSoftwareType = ({
|
|||
/**
|
||||
* This list comprises all possible states of software install operations.
|
||||
*/
|
||||
export const SOFTWARE_UNINSTALL_STATUSES = [
|
||||
"uninstalled",
|
||||
"pending_uninstall",
|
||||
"failed_uninstall",
|
||||
] as const;
|
||||
|
||||
export type SoftwareUninstallStatus = typeof SOFTWARE_UNINSTALL_STATUSES[number];
|
||||
|
||||
export const SOFTWARE_INSTALL_STATUSES = [
|
||||
"failed",
|
||||
"installed",
|
||||
"pending",
|
||||
"pending_install",
|
||||
"failed_install",
|
||||
...SOFTWARE_UNINSTALL_STATUSES,
|
||||
] as const;
|
||||
|
||||
/*
|
||||
|
|
@ -206,26 +217,38 @@ export const SOFTWARE_INSTALL_STATUSES = [
|
|||
export type SoftwareInstallStatus = typeof SOFTWARE_INSTALL_STATUSES[number];
|
||||
|
||||
export const isValidSoftwareInstallStatus = (
|
||||
s: string | undefined
|
||||
s: string | undefined | null
|
||||
): s is SoftwareInstallStatus =>
|
||||
!!s && SOFTWARE_INSTALL_STATUSES.includes(s as SoftwareInstallStatus);
|
||||
|
||||
export const isSoftwareUninstallStatus = (
|
||||
s: string | undefined | null
|
||||
): s is SoftwareUninstallStatus =>
|
||||
!!s && SOFTWARE_UNINSTALL_STATUSES.includes(s as SoftwareUninstallStatus);
|
||||
|
||||
// not a typeguard, as above 2 functions are
|
||||
export const isPendingStatus = (s: string | undefined | null) =>
|
||||
["pending_install", "pending_uninstall"].includes(s || "");
|
||||
|
||||
/**
|
||||
* ISoftwareInstallResult is the shape of a software install result object
|
||||
* returned by the Fleet API.
|
||||
*/
|
||||
export interface ISoftwareInstallResult {
|
||||
host_display_name?: string;
|
||||
install_uuid: string;
|
||||
software_title: string;
|
||||
software_title_id: number;
|
||||
software_package: string;
|
||||
host_id: number;
|
||||
host_display_name: string;
|
||||
status: SoftwareInstallStatus;
|
||||
detail: string;
|
||||
output: string;
|
||||
pre_install_query_output: string;
|
||||
post_install_script_output: string;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
self_service: boolean;
|
||||
}
|
||||
|
||||
export interface ISoftwareInstallResults {
|
||||
|
|
@ -276,16 +299,23 @@ export interface IHostSoftware {
|
|||
app_store_app: IHostAppStoreApp | null;
|
||||
source: string;
|
||||
bundle_identifier?: string;
|
||||
status: SoftwareInstallStatus | null;
|
||||
status: Exclude<SoftwareInstallStatus, "uninstalled"> | null;
|
||||
installed_versions: ISoftwareInstallVersion[] | null;
|
||||
}
|
||||
|
||||
export type IDeviceSoftware = IHostSoftware;
|
||||
|
||||
const INSTALL_STATUS_PREDICATES: Record<SoftwareInstallStatus, string> = {
|
||||
failed: "failed to install",
|
||||
const INSTALL_STATUS_PREDICATES: Record<
|
||||
SoftwareInstallStatus | "pending",
|
||||
string
|
||||
> = {
|
||||
pending: "pending",
|
||||
installed: "installed",
|
||||
pending: "told Fleet to install",
|
||||
uninstalled: "uninstalled",
|
||||
pending_install: "told Fleet to install",
|
||||
failed_install: "failed to install",
|
||||
pending_uninstall: "told Fleet to uninstall",
|
||||
failed_uninstall: "failed to uninstall",
|
||||
} as const;
|
||||
|
||||
export const getInstallStatusPredicate = (status: string | undefined) => {
|
||||
|
|
@ -298,10 +328,18 @@ export const getInstallStatusPredicate = (status: string | undefined) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const INSTALL_STATUS_ICONS: Record<SoftwareInstallStatus, IconNames> = {
|
||||
export const INSTALL_STATUS_ICONS: Record<
|
||||
SoftwareInstallStatus | "pending" | "failed",
|
||||
IconNames
|
||||
> = {
|
||||
pending: "pending-outline",
|
||||
pending_install: "pending-outline",
|
||||
installed: "success-outline",
|
||||
uninstalled: "success-outline",
|
||||
failed: "error-outline",
|
||||
failed_install: "error-outline",
|
||||
pending_uninstall: "pending-outline",
|
||||
failed_uninstall: "error-outline",
|
||||
} as const;
|
||||
|
||||
type IHostSoftwarePackageWithLastInstall = IHostSoftwarePackage & {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import FleetIcon from "components/icons/FleetIcon";
|
|||
|
||||
import { AppInstallDetailsModal } from "components/ActivityDetails/InstallDetails/AppInstallDetails";
|
||||
import { SoftwareInstallDetailsModal } from "components/ActivityDetails/InstallDetails/SoftwareInstallDetails/SoftwareInstallDetails";
|
||||
import SoftwareUninstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal";
|
||||
|
||||
import ActivityItem from "./ActivityItem";
|
||||
import ScriptDetailsModal from "./components/ScriptDetailsModal/ScriptDetailsModal";
|
||||
|
|
@ -41,6 +42,10 @@ const ActivityFeed = ({
|
|||
packageInstallDetails,
|
||||
setPackageInstallDetails,
|
||||
] = useState<IActivityDetails | null>(null);
|
||||
const [
|
||||
packageUninstallDetails,
|
||||
setPackageUninstallDetails,
|
||||
] = useState<IActivityDetails | null>(null);
|
||||
const [
|
||||
appInstallDetails,
|
||||
setAppInstallDetails,
|
||||
|
|
@ -106,6 +111,9 @@ const ActivityFeed = ({
|
|||
case ActivityType.InstalledSoftware:
|
||||
setPackageInstallDetails({ ...details });
|
||||
break;
|
||||
case ActivityType.UninstalledSoftware:
|
||||
setPackageUninstallDetails({ ...details });
|
||||
break;
|
||||
case ActivityType.InstalledAppStoreApp:
|
||||
setAppInstallDetails({ ...details });
|
||||
break;
|
||||
|
|
@ -205,6 +213,12 @@ const ActivityFeed = ({
|
|||
onCancel={() => setPackageInstallDetails(null)}
|
||||
/>
|
||||
)}
|
||||
{packageUninstallDetails && (
|
||||
<SoftwareUninstallDetailsModal
|
||||
details={packageUninstallDetails}
|
||||
onCancel={() => setPackageUninstallDetails(null)}
|
||||
/>
|
||||
)}
|
||||
{appInstallDetails && (
|
||||
<AppInstallDetailsModal
|
||||
details={appInstallDetails}
|
||||
|
|
|
|||
|
|
@ -907,6 +907,40 @@ const TAGGED_TEMPLATES = {
|
|||
</>
|
||||
);
|
||||
},
|
||||
uninstalledSoftware: (
|
||||
activity: IActivity,
|
||||
onDetailsClick?: (type: ActivityType, details: IActivityDetails) => void
|
||||
) => {
|
||||
const { details } = activity;
|
||||
if (!details) {
|
||||
return TAGGED_TEMPLATES.defaultActivityTemplate(activity);
|
||||
}
|
||||
|
||||
const { host_display_name: hostName, software_title: title } = details;
|
||||
const status =
|
||||
details.status === "failed" ? "failed_uninstall" : details.status;
|
||||
|
||||
const showSoftwarePackage =
|
||||
!!details.software_package &&
|
||||
activity.type === ActivityType.InstalledSoftware;
|
||||
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
{getInstallStatusPredicate(status)} software <b>{title}</b>
|
||||
{showSoftwarePackage && ` (${details.software_package})`} from{" "}
|
||||
<b>{hostName}</b>.{" "}
|
||||
<Button
|
||||
className={`${baseClass}__show-query-link`}
|
||||
variant="text-link"
|
||||
onClick={() => onDetailsClick?.(activity.type, details)}
|
||||
>
|
||||
Show details{" "}
|
||||
<Icon className={`${baseClass}__show-query-icon`} name="eye" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
},
|
||||
enabledVpp: (activity: IActivity) => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -1168,6 +1202,9 @@ const getDetail = (
|
|||
case ActivityType.InstalledSoftware: {
|
||||
return TAGGED_TEMPLATES.installedSoftware(activity, onDetailsClick);
|
||||
}
|
||||
case ActivityType.UninstalledSoftware: {
|
||||
return TAGGED_TEMPLATES.uninstalledSoftware(activity, onDetailsClick);
|
||||
}
|
||||
case ActivityType.AddedAppStoreApp: {
|
||||
return TAGGED_TEMPLATES.addedAppStoreApp(activity);
|
||||
}
|
||||
|
|
@ -1234,6 +1271,7 @@ const ActivityItem = ({
|
|||
DEFAULT_ACTOR_DISPLAY
|
||||
);
|
||||
case ActivityType.InstalledSoftware:
|
||||
case ActivityType.UninstalledSoftware:
|
||||
case ActivityType.InstalledAppStoreApp:
|
||||
return activity.details?.self_service ? (
|
||||
<span>An end user</span>
|
||||
|
|
|
|||
|
|
@ -86,8 +86,11 @@ interface IStatusDisplayOption {
|
|||
tooltip: React.ReactNode;
|
||||
}
|
||||
|
||||
// "pending" and "failed" each encompass both "_install" and "_uninstall" sub-statuses
|
||||
type SoftwareInstallDisplayStatus = "installed" | "pending" | "failed";
|
||||
|
||||
const STATUS_DISPLAY_OPTIONS: Record<
|
||||
SoftwareInstallStatus,
|
||||
SoftwareInstallDisplayStatus,
|
||||
IStatusDisplayOption
|
||||
> = {
|
||||
installed: {
|
||||
|
|
@ -106,16 +109,22 @@ const STATUS_DISPLAY_OPTIONS: Record<
|
|||
pending: {
|
||||
displayName: "Pending",
|
||||
iconName: "pending-outline",
|
||||
tooltip: "Fleet is installing or will install when the host comes online.",
|
||||
tooltip: (
|
||||
<>
|
||||
Fleet is installing/uninstalling or will
|
||||
<br />
|
||||
do so when the host comes online.
|
||||
</>
|
||||
),
|
||||
},
|
||||
failed: {
|
||||
displayName: "Failed",
|
||||
iconName: "error",
|
||||
tooltip: (
|
||||
<>
|
||||
These hosts failed to install software. Click on a host to view
|
||||
These hosts failed to install/uninstall software.
|
||||
<br />
|
||||
error(s).
|
||||
Click on a host to view error(s).
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
@ -123,7 +132,7 @@ const STATUS_DISPLAY_OPTIONS: Record<
|
|||
|
||||
interface IPackageStatusCountProps {
|
||||
softwareId: number;
|
||||
status: SoftwareInstallStatus;
|
||||
status: SoftwareInstallDisplayStatus;
|
||||
count: number;
|
||||
teamId?: number;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
import {
|
||||
IAppStoreApp,
|
||||
ISoftwarePackage,
|
||||
ISoftwareTitleDetails,
|
||||
isSoftwarePackage,
|
||||
} from "interfaces/software";
|
||||
import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants";
|
||||
|
||||
const mergePackageStatuses = (packageStatuses: ISoftwarePackage["status"]) => ({
|
||||
installed: packageStatuses.installed,
|
||||
pending: packageStatuses.pending_install + packageStatuses.pending_uninstall,
|
||||
failed: packageStatuses.failed_install + packageStatuses.failed_uninstall,
|
||||
});
|
||||
/**
|
||||
* Generates the data needed to render the package card.
|
||||
*/
|
||||
|
|
@ -24,7 +30,9 @@ export const getPackageCardInfo = (softwareTitle: ISoftwareTitleDetails) => {
|
|||
? packageData.version
|
||||
: packageData.latest_version) || DEFAULT_EMPTY_CELL_VALUE,
|
||||
uploadedAt: isSoftwarePackage(packageData) ? packageData.uploaded_at : "",
|
||||
status: packageData.status,
|
||||
status: isSoftwarePackage(packageData)
|
||||
? mergePackageStatuses(packageData.status)
|
||||
: packageData.status,
|
||||
isSelfService: packageData.self_service,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,33 +1,160 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
|
||||
|
||||
import {
|
||||
isPackageType,
|
||||
isWindowsPackageType,
|
||||
PackageType,
|
||||
} from "interfaces/package_type";
|
||||
|
||||
import Editor from "components/Editor";
|
||||
import CustomLink from "components/CustomLink";
|
||||
import FleetAce from "components/FleetAce";
|
||||
import RevealButton from "components/buttons/RevealButton";
|
||||
import { IAddPackageFormData } from "../AddPackageForm/AddPackageForm";
|
||||
|
||||
const getSupportedScriptTypeText = (pkgType: PackageType) => {
|
||||
return `Currently, ${
|
||||
isWindowsPackageType(pkgType) ? "PowerS" : "s"
|
||||
}hell scripts are supported.`;
|
||||
};
|
||||
|
||||
const PKG_TYPE_TO_ID_TEXT = {
|
||||
pkg: "package IDs",
|
||||
deb: "package name",
|
||||
msi: "product code",
|
||||
exe: "software name",
|
||||
} as const;
|
||||
|
||||
const getInstallHelpText = (pkgType: PackageType) => (
|
||||
<>
|
||||
Use the $INSTALLER_PATH variable to point to the installer.{" "}
|
||||
{getSupportedScriptTypeText(pkgType)}{" "}
|
||||
<CustomLink
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/install-scripts`}
|
||||
text="Learn more about install scripts"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
const getPostInstallHelpText = (pkgType: PackageType) => {
|
||||
return getSupportedScriptTypeText(pkgType);
|
||||
};
|
||||
|
||||
const getUninstallHelpText = (pkgType: PackageType) => {
|
||||
return (
|
||||
<>
|
||||
$PACKAGE_ID will be populated with the {PKG_TYPE_TO_ID_TEXT[pkgType]} from
|
||||
the .{pkgType} file after the software is added.{" "}
|
||||
{getSupportedScriptTypeText(pkgType)}{" "}
|
||||
<CustomLink
|
||||
url={`${LEARN_MORE_ABOUT_BASE_LINK}/uninstall-scripts`}
|
||||
text="Learn more about uninstall scripts"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const baseClass = "add-package-advanced-options";
|
||||
|
||||
interface IAddPackageAdvancedOptionsProps {
|
||||
errors: { preInstallQuery?: string; postInstallScript?: string };
|
||||
selectedPackage: IAddPackageFormData["software"];
|
||||
preInstallQuery?: string;
|
||||
installScript: string;
|
||||
postInstallScript?: string;
|
||||
uninstallScript?: string;
|
||||
onChangePreInstallQuery: (value?: string) => void;
|
||||
onChangeInstallScript: (value: string) => void;
|
||||
onChangePostInstallScript: (value?: string) => void;
|
||||
onChangeUninstallScript: (value?: string) => void;
|
||||
}
|
||||
|
||||
const AddPackageAdvancedOptions = ({
|
||||
errors,
|
||||
selectedPackage,
|
||||
preInstallQuery,
|
||||
installScript,
|
||||
postInstallScript,
|
||||
uninstallScript,
|
||||
onChangePreInstallQuery,
|
||||
onChangeInstallScript,
|
||||
onChangePostInstallScript,
|
||||
onChangeUninstallScript,
|
||||
}: IAddPackageAdvancedOptionsProps) => {
|
||||
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
|
||||
|
||||
const renderAdvancedOptions = () => {
|
||||
const name = selectedPackage?.name || "";
|
||||
const ext = name.split(".").pop() as PackageType;
|
||||
if (!isPackageType(ext)) {
|
||||
// this should never happen
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={`${baseClass}__input-fields`}>
|
||||
<FleetAce
|
||||
className="form-field"
|
||||
focus
|
||||
error={errors.preInstallQuery}
|
||||
value={preInstallQuery}
|
||||
placeholder="SELECT * FROM osquery_info WHERE start_time > 1"
|
||||
label="Pre-install query"
|
||||
name="preInstallQuery"
|
||||
maxLines={10}
|
||||
onChange={onChangePreInstallQuery}
|
||||
helpText={
|
||||
<>
|
||||
Software will be installed only if the{" "}
|
||||
<CustomLink
|
||||
className={`${baseClass}__table-link`}
|
||||
text="query returns results"
|
||||
url="https://fleetdm.com/tables"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Editor
|
||||
wrapEnabled
|
||||
maxLines={10}
|
||||
name="install-script"
|
||||
onChange={onChangeInstallScript}
|
||||
value={installScript}
|
||||
helpText={getInstallHelpText(ext)}
|
||||
label="Install script"
|
||||
isFormField
|
||||
/>
|
||||
<Editor
|
||||
label="Post-install script"
|
||||
focus
|
||||
error={errors.postInstallScript}
|
||||
wrapEnabled
|
||||
name="post-install-script-editor"
|
||||
maxLines={10}
|
||||
onChange={onChangePostInstallScript}
|
||||
value={postInstallScript}
|
||||
helpText={getPostInstallHelpText(ext)}
|
||||
isFormField
|
||||
/>
|
||||
<Editor
|
||||
label="Uninstall script"
|
||||
focus
|
||||
wrapEnabled
|
||||
name="uninstall-script-editor"
|
||||
maxLines={20}
|
||||
onChange={onChangeUninstallScript}
|
||||
value={uninstallScript}
|
||||
helpText={getUninstallHelpText(ext)}
|
||||
isFormField
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<RevealButton
|
||||
|
|
@ -37,63 +164,14 @@ const AddPackageAdvancedOptions = ({
|
|||
hideText="Advanced options"
|
||||
caretPosition="after"
|
||||
onClick={() => setShowAdvancedOptions(!showAdvancedOptions)}
|
||||
disabled={!selectedPackage}
|
||||
disabledTooltipContent={
|
||||
selectedPackage
|
||||
? "Choose a file to modify advanced options."
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{showAdvancedOptions && (
|
||||
<div className={`${baseClass}__input-fields`}>
|
||||
<FleetAce
|
||||
className="form-field"
|
||||
focus
|
||||
error={errors.preInstallQuery}
|
||||
value={preInstallQuery}
|
||||
placeholder="SELECT * FROM osquery_info WHERE start_time > 1"
|
||||
label="Pre-install query"
|
||||
name="preInstallQuery"
|
||||
maxLines={10}
|
||||
onChange={onChangePreInstallQuery}
|
||||
helpText={
|
||||
<>
|
||||
Software will be installed only if the{" "}
|
||||
<CustomLink
|
||||
className={`${baseClass}__table-link`}
|
||||
text="query returns results"
|
||||
url="https://fleetdm.com/tables"
|
||||
newTab
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<Editor
|
||||
wrapEnabled
|
||||
maxLines={10}
|
||||
name="install-script"
|
||||
onChange={onChangeInstallScript}
|
||||
value={installScript}
|
||||
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
|
||||
label="Install script"
|
||||
labelTooltip={
|
||||
<>
|
||||
Fleet will run this script on hosts to install software. Use the
|
||||
<br />
|
||||
$INSTALLER_PATH variable to point to the installer.
|
||||
</>
|
||||
}
|
||||
isFormField
|
||||
/>
|
||||
<Editor
|
||||
label="Post-install script"
|
||||
labelTooltip="Fleet will run this script after install."
|
||||
focus
|
||||
error={errors.postInstallScript}
|
||||
wrapEnabled
|
||||
name="post-install-script-editor"
|
||||
maxLines={10}
|
||||
onChange={onChangePostInstallScript}
|
||||
value={postInstallScript}
|
||||
helpText="Shell (macOS and Linux) or PowerShell (Windows)."
|
||||
isFormField
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAdvancedOptions && !!selectedPackage && renderAdvancedOptions()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@ import React, { useContext, useState } from "react";
|
|||
|
||||
import { NotificationContext } from "context/notification";
|
||||
import { getFileDetails } from "utilities/file/fileUtils";
|
||||
import getInstallScript from "utilities/software_install_scripts";
|
||||
import getDefaultInstallScript from "utilities/software_install_scripts";
|
||||
import getDefaultUninstallScript from "utilities/software_uninstall_scripts";
|
||||
|
||||
import Button from "components/buttons/Button";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
|
|
@ -30,9 +31,10 @@ const UploadingSoftware = () => {
|
|||
|
||||
export interface IAddPackageFormData {
|
||||
software: File | null;
|
||||
installScript: string;
|
||||
preInstallQuery?: string;
|
||||
installScript: string;
|
||||
postInstallScript?: string;
|
||||
uninstallScript?: string;
|
||||
selfService: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -59,9 +61,10 @@ const AddPackageForm = ({
|
|||
|
||||
const [formData, setFormData] = useState<IAddPackageFormData>({
|
||||
software: null,
|
||||
installScript: "",
|
||||
preInstallQuery: undefined,
|
||||
installScript: "",
|
||||
postInstallScript: undefined,
|
||||
uninstallScript: undefined,
|
||||
selfService: false,
|
||||
});
|
||||
const [formValidation, setFormValidation] = useState<IFormValidation>({
|
||||
|
|
@ -69,13 +72,21 @@ const AddPackageForm = ({
|
|||
software: { isValid: false },
|
||||
});
|
||||
|
||||
const onFileUpload = (files: FileList | null) => {
|
||||
const onFileSelect = (files: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
|
||||
let installScript: string;
|
||||
let defaultInstallScript: string;
|
||||
try {
|
||||
installScript = getInstallScript(file.name);
|
||||
defaultInstallScript = getDefaultInstallScript(file.name);
|
||||
} catch (e) {
|
||||
renderFlash("error", `${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
let defaultUninstallScript: string;
|
||||
try {
|
||||
defaultUninstallScript = getDefaultUninstallScript(file.name);
|
||||
} catch (e) {
|
||||
renderFlash("error", `${e}`);
|
||||
return;
|
||||
|
|
@ -84,7 +95,8 @@ const AddPackageForm = ({
|
|||
const newData = {
|
||||
...formData,
|
||||
software: file,
|
||||
installScript,
|
||||
installScript: defaultInstallScript,
|
||||
uninstallScript: defaultUninstallScript,
|
||||
};
|
||||
setFormData(newData);
|
||||
setFormValidation(generateFormValidation(newData));
|
||||
|
|
@ -112,6 +124,12 @@ const AddPackageForm = ({
|
|||
setFormValidation(generateFormValidation(newData));
|
||||
};
|
||||
|
||||
const onChangeUninstallScript = (value?: string) => {
|
||||
const newData = { ...formData, uninstallScript: value };
|
||||
setFormData(newData);
|
||||
setFormValidation(generateFormValidation(newData));
|
||||
};
|
||||
|
||||
const onToggleSelfServiceCheckbox = (value: boolean) => {
|
||||
const newData = { ...formData, selfService: value };
|
||||
setFormData(newData);
|
||||
|
|
@ -130,7 +148,7 @@ const AddPackageForm = ({
|
|||
graphicName={"file-pkg"}
|
||||
accept=".pkg,.msi,.exe,.deb"
|
||||
message=".pkg, .msi, .exe, or .deb"
|
||||
onFileUpload={onFileUpload}
|
||||
onFileUpload={onFileSelect}
|
||||
buttonMessage="Choose file"
|
||||
buttonType="link"
|
||||
className={`${baseClass}__file-uploader`}
|
||||
|
|
@ -156,16 +174,19 @@ const AddPackageForm = ({
|
|||
</TooltipWrapper>
|
||||
</Checkbox>
|
||||
<AddPackageAdvancedOptions
|
||||
selectedPackage={formData.software}
|
||||
errors={{
|
||||
preInstallQuery: formValidation.preInstallQuery?.message,
|
||||
postInstallScript: formValidation.postInstallScript?.message,
|
||||
}}
|
||||
preInstallQuery={formData.preInstallQuery}
|
||||
installScript={formData.installScript}
|
||||
postInstallScript={formData.postInstallScript}
|
||||
uninstallScript={formData.uninstallScript}
|
||||
onChangePreInstallQuery={onChangePreInstallQuery}
|
||||
onChangeInstallScript={onChangeInstallScript}
|
||||
onChangePostInstallScript={onChangePostInstallScript}
|
||||
installScript={formData.installScript}
|
||||
onChangeUninstallScript={onChangeUninstallScript}
|
||||
/>
|
||||
<div className="modal-cta-wrap">
|
||||
<Button type="submit" variant="brand" disabled={isSubmitDisabled}>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import validator from "validator";
|
||||
|
||||
// @ts-ignore
|
||||
import validateQuery from "components/forms/validators/validate_query";
|
||||
|
||||
|
|
@ -7,7 +5,7 @@ import { IAddPackageFormData, IFormValidation } from "./AddPackageForm";
|
|||
|
||||
type IAddPackageFormValidatorKey = Exclude<
|
||||
keyof IAddPackageFormData,
|
||||
"installScript"
|
||||
"installScript" | "uninstallScript"
|
||||
>;
|
||||
|
||||
type IMessageFunc = (formData: IAddPackageFormData) => string;
|
||||
|
|
|
|||
|
|
@ -415,7 +415,7 @@ const DeviceUserPage = ({
|
|||
<SoftwareCard
|
||||
id={deviceAuthToken}
|
||||
softwareUpdatedAt={host.software_updated_at}
|
||||
hostCanInstallSoftware={!!host.orbit_version}
|
||||
hostCanWriteSoftware={!!host.orbit_version}
|
||||
router={router}
|
||||
pathname={location.pathname}
|
||||
queryParams={parseHostSoftwareQueryParams(location.query)}
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import {
|
|||
SoftwareInstallDetailsModal,
|
||||
IPackageInstallDetails,
|
||||
} from "components/ActivityDetails/InstallDetails/SoftwareInstallDetails/SoftwareInstallDetails";
|
||||
import SoftwareUninstallDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal";
|
||||
|
||||
import HostSummaryCard from "../cards/HostSummary";
|
||||
import AboutCard from "../cards/About";
|
||||
|
|
@ -181,6 +182,10 @@ const HostDetailsPage = ({
|
|||
packageInstallDetails,
|
||||
setPackageInstallDetails,
|
||||
] = useState<IPackageInstallDetails | null>(null);
|
||||
const [
|
||||
packageUninstallDetails,
|
||||
setPackageUninstallDetails,
|
||||
] = useState<IPackageInstallDetails | null>(null);
|
||||
const [
|
||||
appInstallDetails,
|
||||
setAppInstallDetails,
|
||||
|
|
@ -602,6 +607,13 @@ const HostDetailsPage = ({
|
|||
host?.display_name || details?.host_display_name || "",
|
||||
});
|
||||
break;
|
||||
case "uninstalled_software":
|
||||
setPackageUninstallDetails({
|
||||
...details,
|
||||
host_display_name:
|
||||
host?.display_name || details?.host_display_name || "",
|
||||
});
|
||||
break;
|
||||
case "installed_app_store_app":
|
||||
setAppInstallDetails({
|
||||
...details,
|
||||
|
|
@ -933,9 +945,7 @@ const HostDetailsPage = ({
|
|||
id={host.id}
|
||||
platform={host.platform}
|
||||
softwareUpdatedAt={host.software_updated_at}
|
||||
hostCanInstallSoftware={
|
||||
!!host.orbit_version || isIosOrIpadosHost
|
||||
}
|
||||
hostCanWriteSoftware={!!host.orbit_version || isIosOrIpadosHost}
|
||||
isSoftwareEnabled={featuresConfig?.enable_software_inventory}
|
||||
router={router}
|
||||
queryParams={parseHostSoftwareQueryParams(location.query)}
|
||||
|
|
@ -1065,6 +1075,12 @@ const HostDetailsPage = ({
|
|||
onCancel={onCancelSoftwareInstallDetailsModal}
|
||||
/>
|
||||
)}
|
||||
{packageUninstallDetails && (
|
||||
<SoftwareUninstallDetailsModal
|
||||
details={packageUninstallDetails}
|
||||
onCancel={() => setPackageUninstallDetails(null)}
|
||||
/>
|
||||
)}
|
||||
{!!appInstallDetails && (
|
||||
<AppInstallDetailsModal
|
||||
details={appInstallDetails}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const pastActivityComponentMap: Record<
|
|||
[ActivityType.LockedHost]: LockedHostActivityItem,
|
||||
[ActivityType.UnlockedHost]: UnlockedHostActivityItem,
|
||||
[ActivityType.InstalledSoftware]: InstalledSoftwareActivityItem,
|
||||
[ActivityType.UninstalledSoftware]: InstalledSoftwareActivityItem,
|
||||
[ActivityType.InstalledAppStoreApp]: InstalledSoftwareActivityItem,
|
||||
};
|
||||
|
||||
|
|
@ -46,5 +47,6 @@ export const upcomingActivityComponentMap: Record<
|
|||
> = {
|
||||
[ActivityType.RanScript]: RanScriptActivityItem,
|
||||
[ActivityType.InstalledSoftware]: InstalledSoftwareActivityItem,
|
||||
[ActivityType.UninstalledSoftware]: InstalledSoftwareActivityItem,
|
||||
[ActivityType.InstalledAppStoreApp]: InstalledSoftwareActivityItem,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ const InstalledSoftwareActivityItem = ({
|
|||
onShowDetails,
|
||||
}: IHostActivityItemComponentPropsWithShowDetails) => {
|
||||
const { actor_full_name: actorName, details } = activity;
|
||||
const { self_service, status, software_title: title } = details;
|
||||
const { self_service, software_title: title } = details;
|
||||
const status =
|
||||
details.status === "failed" ? "failed_uninstall" : details.status;
|
||||
|
||||
const actorDisplayName = self_service ? (
|
||||
<span>An end user</span>
|
||||
|
|
|
|||
|
|
@ -441,7 +441,6 @@ const HostSummary = ({
|
|||
};
|
||||
|
||||
const renderSummary = () => {
|
||||
console.log(hostMdmProfiles);
|
||||
// for windows hosts we have to manually add a profile for disk encryption
|
||||
// as this is not currently included in the `profiles` value from the API
|
||||
// response for windows hosts.
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ interface IHostSoftwareProps {
|
|||
id: number | string;
|
||||
platform?: HostPlatform;
|
||||
softwareUpdatedAt?: string;
|
||||
hostCanInstallSoftware: boolean;
|
||||
hostCanWriteSoftware: boolean;
|
||||
router: InjectedRouter;
|
||||
queryParams: ReturnType<typeof parseHostSoftwareQueryParams>;
|
||||
pathname: string;
|
||||
|
|
@ -86,7 +86,7 @@ const HostSoftware = ({
|
|||
id,
|
||||
platform,
|
||||
softwareUpdatedAt,
|
||||
hostCanInstallSoftware,
|
||||
hostCanWriteSoftware,
|
||||
router,
|
||||
queryParams,
|
||||
pathname,
|
||||
|
|
@ -105,7 +105,8 @@ const HostSoftware = ({
|
|||
isTeamMaintainer,
|
||||
} = useContext(AppContext);
|
||||
|
||||
const [installingSoftwareId, setInstallingSoftwareId] = useState<
|
||||
// disables install/uninstall actions after click
|
||||
const [softwareIdActionPending, setSoftwareIdActionPending] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
|
||||
|
|
@ -175,13 +176,13 @@ const HostSoftware = ({
|
|||
[isMyDevicePage, refetchDeviceSoftware, refetchHostSoftware]
|
||||
);
|
||||
|
||||
const userHasSWInstallPermission = Boolean(
|
||||
const userHasSWWritePermission = Boolean(
|
||||
isGlobalAdmin || isGlobalMaintainer || isTeamAdmin || isTeamMaintainer
|
||||
);
|
||||
|
||||
const installHostSoftwarePackage = useCallback(
|
||||
async (softwareId: number) => {
|
||||
setInstallingSoftwareId(softwareId);
|
||||
setSoftwareIdActionPending(softwareId);
|
||||
try {
|
||||
await hostAPI.installHostSoftwarePackage(id as number, softwareId);
|
||||
renderFlash(
|
||||
|
|
@ -191,7 +192,28 @@ const HostSoftware = ({
|
|||
} catch (e) {
|
||||
renderFlash("error", getErrorMessage(e));
|
||||
}
|
||||
setInstallingSoftwareId(null);
|
||||
setSoftwareIdActionPending(null);
|
||||
refetchSoftware();
|
||||
},
|
||||
[id, renderFlash, refetchSoftware]
|
||||
);
|
||||
|
||||
const uninstallHostSoftwarePackage = useCallback(
|
||||
async (softwareId: number) => {
|
||||
setSoftwareIdActionPending(softwareId);
|
||||
try {
|
||||
await hostAPI.uninstallHostSoftwarePackage(id as number, softwareId);
|
||||
renderFlash(
|
||||
"success",
|
||||
<>
|
||||
Software is uninstalling or will uninstall when the host comes
|
||||
online. To see details, go to <b>Details > Activity</b>.
|
||||
</>
|
||||
);
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn't uninstall. Please try again.");
|
||||
}
|
||||
setSoftwareIdActionPending(null);
|
||||
refetchSoftware();
|
||||
},
|
||||
[id, renderFlash, refetchSoftware]
|
||||
|
|
@ -203,6 +225,9 @@ const HostSoftware = ({
|
|||
case "install":
|
||||
installHostSoftwarePackage(software.id);
|
||||
break;
|
||||
case "uninstall":
|
||||
uninstallHostSoftwarePackage(software.id);
|
||||
break;
|
||||
case "showDetails":
|
||||
onShowSoftwareDetails?.(software);
|
||||
break;
|
||||
|
|
@ -210,7 +235,11 @@ const HostSoftware = ({
|
|||
break;
|
||||
}
|
||||
},
|
||||
[installHostSoftwarePackage, onShowSoftwareDetails]
|
||||
[
|
||||
installHostSoftwarePackage,
|
||||
onShowSoftwareDetails,
|
||||
uninstallHostSoftwarePackage,
|
||||
]
|
||||
);
|
||||
|
||||
const tableConfig = useMemo(() => {
|
||||
|
|
@ -218,20 +247,20 @@ const HostSoftware = ({
|
|||
? generateDeviceSoftwareTableConfig()
|
||||
: generateHostSoftwareTableConfig({
|
||||
router,
|
||||
installingSoftwareId,
|
||||
userHasSWInstallPermission,
|
||||
softwareIdActionPending,
|
||||
userHasSWWritePermission,
|
||||
onSelectAction,
|
||||
teamId: hostTeamId,
|
||||
hostCanInstallSoftware,
|
||||
hostCanWriteSoftware,
|
||||
});
|
||||
}, [
|
||||
isMyDevicePage,
|
||||
router,
|
||||
installingSoftwareId,
|
||||
userHasSWInstallPermission,
|
||||
softwareIdActionPending,
|
||||
userHasSWWritePermission,
|
||||
onSelectAction,
|
||||
hostTeamId,
|
||||
hostCanInstallSoftware,
|
||||
hostCanWriteSoftware,
|
||||
]);
|
||||
|
||||
const isLoading = isMyDevicePage
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import InstallStatusCell from "./InstallStatusCell";
|
|||
const DEFAULT_ACTION_OPTIONS: IDropdownOption[] = [
|
||||
{ value: "showDetails", label: "Show details", disabled: false },
|
||||
{ value: "install", label: "Install", disabled: false },
|
||||
{ value: "uninstall", label: "Uninstall", disabled: false },
|
||||
];
|
||||
|
||||
type ISoftwareTableConfig = Column<IHostSoftware>;
|
||||
|
|
@ -50,17 +51,18 @@ type IInstalledVersionsCellProps = CellProps<
|
|||
type IVulnerabilitiesCellProps = IInstalledVersionsCellProps;
|
||||
|
||||
const generateActions = ({
|
||||
userHasSWInstallPermission,
|
||||
hostCanInstallSoftware,
|
||||
installingSoftwareId,
|
||||
userHasSWWritePermission,
|
||||
// Commenting below in case there is a quick decision to use these conditions after all
|
||||
// hostCanWriteSoftware,
|
||||
// software_package,
|
||||
softwareIdActionPending,
|
||||
softwareId,
|
||||
status,
|
||||
software_package,
|
||||
app_store_app,
|
||||
}: {
|
||||
userHasSWInstallPermission: boolean;
|
||||
hostCanInstallSoftware: boolean;
|
||||
installingSoftwareId: number | null;
|
||||
userHasSWWritePermission: boolean;
|
||||
hostCanWriteSoftware: boolean;
|
||||
softwareIdActionPending: number | null;
|
||||
softwareId: number;
|
||||
status: SoftwareInstallStatus | null;
|
||||
software_package: IHostSoftwarePackage | null;
|
||||
|
|
@ -76,39 +78,44 @@ const generateActions = ({
|
|||
// error to fail loudly so that we know to update this function
|
||||
throw new Error("Install action not found in default actions");
|
||||
}
|
||||
const indexUninstallAction = actions.findIndex(
|
||||
(a) => a.value === "uninstall"
|
||||
);
|
||||
if (indexUninstallAction === -1) {
|
||||
// this should never happen unless the default actions change, but if it does we'll throw an
|
||||
// error to fail loudly so that we know to update this function
|
||||
throw new Error("Uninstall action not found in default actions");
|
||||
}
|
||||
|
||||
const hasSoftwareToInstall = !!software_package || !!app_store_app;
|
||||
// remove install if there is no package to install or if the software is already installed
|
||||
if (
|
||||
!hasSoftwareToInstall ||
|
||||
!userHasSWInstallPermission ||
|
||||
status === "installed"
|
||||
) {
|
||||
if (!userHasSWWritePermission) {
|
||||
actions.splice(indexInstallAction, 1);
|
||||
return actions;
|
||||
actions.splice(indexUninstallAction, 1);
|
||||
} else {
|
||||
// user has software write permission for host
|
||||
const pendingStatuses = ["pending_install", "pending_uninstall"];
|
||||
|
||||
if (
|
||||
// if locally pending (waiting for API response) or pending install/uninstall, disable both
|
||||
// install and uninstall
|
||||
softwareId === softwareIdActionPending ||
|
||||
pendingStatuses.includes(status || "")
|
||||
) {
|
||||
actions[indexInstallAction].disabled = true;
|
||||
actions[indexUninstallAction].disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// disable install option if not a fleetd, iPad, or iOS host
|
||||
if (!hostCanInstallSoftware) {
|
||||
actions[indexInstallAction].disabled = true;
|
||||
actions[indexInstallAction].tooltipContent =
|
||||
"To install software on this host, deploy the fleetd agent with --enable-scripts and refetch host vitals.";
|
||||
return actions;
|
||||
if (app_store_app) {
|
||||
// remove uninstall for VPP apps
|
||||
actions.splice(indexUninstallAction, 1);
|
||||
}
|
||||
|
||||
// disable install option if software is already installing
|
||||
if (softwareId === installingSoftwareId || status === "pending") {
|
||||
actions[indexInstallAction].disabled = true;
|
||||
return actions;
|
||||
}
|
||||
|
||||
return actions;
|
||||
};
|
||||
|
||||
interface ISoftwareTableHeadersProps {
|
||||
userHasSWInstallPermission: boolean;
|
||||
hostCanInstallSoftware: boolean;
|
||||
installingSoftwareId: number | null;
|
||||
userHasSWWritePermission: boolean;
|
||||
hostCanWriteSoftware: boolean;
|
||||
softwareIdActionPending: number | null;
|
||||
router: InjectedRouter;
|
||||
teamId: number;
|
||||
onSelectAction: (software: IHostSoftware, action: string) => void;
|
||||
|
|
@ -117,9 +124,9 @@ interface ISoftwareTableHeadersProps {
|
|||
// NOTE: cellProps come from react-table
|
||||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
export const generateSoftwareTableHeaders = ({
|
||||
userHasSWInstallPermission,
|
||||
hostCanInstallSoftware,
|
||||
installingSoftwareId,
|
||||
userHasSWWritePermission,
|
||||
hostCanWriteSoftware,
|
||||
softwareIdActionPending,
|
||||
router,
|
||||
teamId,
|
||||
onSelectAction,
|
||||
|
|
@ -209,9 +216,9 @@ export const generateSoftwareTableHeaders = ({
|
|||
<DropdownCell
|
||||
placeholder="Actions"
|
||||
options={generateActions({
|
||||
userHasSWInstallPermission,
|
||||
hostCanInstallSoftware,
|
||||
installingSoftwareId,
|
||||
userHasSWWritePermission,
|
||||
hostCanWriteSoftware,
|
||||
softwareIdActionPending,
|
||||
softwareId,
|
||||
status,
|
||||
software_package,
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export type IStatusDisplayConfig = {
|
|||
};
|
||||
|
||||
export const INSTALL_STATUS_DISPLAY_OPTIONS: Record<
|
||||
IStatusValue | "selfService",
|
||||
Exclude<IStatusValue, "uninstalled"> | "selfService",
|
||||
IStatusDisplayConfig
|
||||
> = {
|
||||
installed: {
|
||||
|
|
@ -39,20 +39,42 @@ export const INSTALL_STATUS_DISPLAY_OPTIONS: Record<
|
|||
tooltip: () =>
|
||||
"Software is installed (install script finished with exit code 0).",
|
||||
},
|
||||
pending: {
|
||||
pending_install: {
|
||||
iconName: "pending-outline",
|
||||
displayText: "Pending",
|
||||
displayText: "Installing (pending)",
|
||||
tooltip: () =>
|
||||
"Fleet is installing or will install when the host comes online.",
|
||||
},
|
||||
failed: {
|
||||
iconName: "error",
|
||||
displayText: "Failed",
|
||||
pending_uninstall: {
|
||||
iconName: "pending-outline",
|
||||
displayText: "Uninstalling (pending)",
|
||||
tooltip: () => (
|
||||
<>
|
||||
The host failed to install software. To view errors, select
|
||||
Fleet is uninstalling or will uninstall
|
||||
<br />
|
||||
<b>Actions > Show details</b>.
|
||||
software when the host comes online.
|
||||
</>
|
||||
),
|
||||
},
|
||||
failed_install: {
|
||||
iconName: "error",
|
||||
displayText: "Install (failed)",
|
||||
tooltip: () => (
|
||||
<>
|
||||
The host failed to install software.
|
||||
<br />
|
||||
Select <b>Actions > Show details</b> view errors.
|
||||
</>
|
||||
),
|
||||
},
|
||||
failed_uninstall: {
|
||||
iconName: "error",
|
||||
displayText: "Uninstall (failed)",
|
||||
tooltip: () => (
|
||||
<>
|
||||
The host failed to uninstall software.
|
||||
<br />
|
||||
Select <b>Details > Activity</b> to view errors.
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -112,13 +112,13 @@ describe("SelfService", () => {
|
|||
).toHaveTextContent("Reinstall");
|
||||
});
|
||||
|
||||
it("renders 'Retry' action button with 'Failed' status", async () => {
|
||||
it("renders 'Retry' action button with 'failed_install' status", async () => {
|
||||
mockServer.use(
|
||||
customDeviceSoftwareHandler({
|
||||
software: [
|
||||
createMockDeviceSoftware({
|
||||
name: "test-software",
|
||||
status: "failed",
|
||||
status: "failed_install",
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
@ -166,13 +166,13 @@ describe("SelfService", () => {
|
|||
).toHaveTextContent("Install");
|
||||
});
|
||||
|
||||
it("renders no action button with 'Pending' status", async () => {
|
||||
it("renders no action button with 'pending_install' status", async () => {
|
||||
mockServer.use(
|
||||
customDeviceSoftwareHandler({
|
||||
software: [
|
||||
createMockDeviceSoftware({
|
||||
name: "test-software",
|
||||
status: "pending",
|
||||
status: "pending_install",
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,24 +21,30 @@ import { IStatusDisplayConfig } from "../../InstallStatusCell/InstallStatusCell"
|
|||
|
||||
const baseClass = "self-service-item";
|
||||
|
||||
const STATUS_CONFIG: Record<SoftwareInstallStatus, IStatusDisplayConfig> = {
|
||||
const STATUS_CONFIG: Record<
|
||||
Exclude<
|
||||
SoftwareInstallStatus,
|
||||
"pending_uninstall" | "failed_uninstall" | "uninstalled"
|
||||
>,
|
||||
IStatusDisplayConfig
|
||||
> = {
|
||||
installed: {
|
||||
iconName: "success",
|
||||
displayText: "Installed",
|
||||
tooltip: ({ lastInstalledAt }) =>
|
||||
`Software is installed (${dateAgo(lastInstalledAt as string)}).`,
|
||||
},
|
||||
pending: {
|
||||
pending_install: {
|
||||
iconName: "pending-outline",
|
||||
displayText: "Pending",
|
||||
tooltip: () => "Fleet is installing software.",
|
||||
},
|
||||
failed: {
|
||||
failed_install: {
|
||||
iconName: "error",
|
||||
displayText: "Failed",
|
||||
tooltip: ({ lastInstalledAt = "" }) => (
|
||||
<>
|
||||
Software failed to install{" "}
|
||||
Software failed to install
|
||||
{lastInstalledAt ? ` (${dateAgo(lastInstalledAt)})` : ""}. Select{" "}
|
||||
<b>Retry</b> to install again, or contact your IT department.
|
||||
</>
|
||||
|
|
@ -134,7 +140,7 @@ const getInstallButtonText = (status: SoftwareInstallStatus | null) => {
|
|||
switch (status) {
|
||||
case null:
|
||||
return "Install";
|
||||
case "failed":
|
||||
case "failed_install":
|
||||
return "Retry";
|
||||
case "installed":
|
||||
return "Reinstall";
|
||||
|
|
@ -163,7 +169,7 @@ const InstallerStatusAction = ({
|
|||
|
||||
// if the localStatus is "failed", we don't want our tooltip to include the old installed_at date so we
|
||||
// set this to null, which tells the tooltip to omit the parenthetical date
|
||||
const lastInstall = localStatus === "failed" ? null : last_install;
|
||||
const lastInstall = localStatus === "failed_install" ? null : last_install;
|
||||
|
||||
const isMountedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
|
|
@ -174,7 +180,7 @@ const InstallerStatusAction = ({
|
|||
}, []);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
setLocalStatus("pending");
|
||||
setLocalStatus("pending_install");
|
||||
try {
|
||||
await deviceApi.installSelfServiceSoftware(deviceToken, id);
|
||||
if (isMountedRef.current) {
|
||||
|
|
@ -183,7 +189,7 @@ const InstallerStatusAction = ({
|
|||
} catch (error) {
|
||||
renderFlash("error", "Couldn't install. Please try again.");
|
||||
if (isMountedRef.current) {
|
||||
setLocalStatus("failed");
|
||||
setLocalStatus("failed_install");
|
||||
}
|
||||
}
|
||||
}, [deviceToken, id, onInstall, renderFlash]);
|
||||
|
|
@ -200,7 +206,7 @@ const InstallerStatusAction = ({
|
|||
type="button"
|
||||
className={`${baseClass}__item-action-button`}
|
||||
onClick={onClick}
|
||||
disabled={localStatus === "pending"}
|
||||
disabled={localStatus === "pending_install"}
|
||||
>
|
||||
<span data-testid={`${baseClass}__item-action-button--test`}>
|
||||
{installButtonText}
|
||||
|
|
|
|||
|
|
@ -590,4 +590,11 @@ export default {
|
|||
HOST_SOFTWARE_PACKAGE_INSTALL(hostId, softwareId)
|
||||
);
|
||||
},
|
||||
uninstallHostSoftwarePackage: (hostId: number, softwareId: number) => {
|
||||
const { HOST_SOFTWARE_PACKAGE_UNINSTALL } = endpoints;
|
||||
return sendRequest(
|
||||
"POST",
|
||||
HOST_SOFTWARE_PACKAGE_UNINSTALL(hostId, softwareId)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export interface IScriptResultResponse {
|
|||
message: string;
|
||||
runtime: number;
|
||||
host_timeout: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ export default {
|
|||
formData.append("software", data.software);
|
||||
formData.append("self_service", data.selfService.toString());
|
||||
data.installScript && formData.append("install_script", data.installScript);
|
||||
data.uninstallScript &&
|
||||
formData.append("uninstall_script", data.uninstallScript);
|
||||
data.preInstallQuery &&
|
||||
formData.append("pre_install_query", data.preInstallQuery);
|
||||
data.postInstallScript &&
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@ $max-width: 2560px;
|
|||
font-size: $xx-small;
|
||||
font-weight: $regular;
|
||||
@include grey-text;
|
||||
.custom-link {
|
||||
font-size: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin link {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,9 @@ export default {
|
|||
`/${API_VERSION}/fleet/hosts/${hostId}/configuration_profiles/resend/${profileUUID}`,
|
||||
HOST_SOFTWARE: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/software`,
|
||||
HOST_SOFTWARE_PACKAGE_INSTALL: (hostId: number, softwareId: number) =>
|
||||
`/${API_VERSION}/fleet/hosts/${hostId}/software/install/${softwareId}`,
|
||||
`/${API_VERSION}/fleet/hosts/${hostId}/software/${softwareId}/install`,
|
||||
HOST_SOFTWARE_PACKAGE_UNINSTALL: (hostId: number, softwareId: number) =>
|
||||
`/${API_VERSION}/fleet/hosts/${hostId}/software/${softwareId}/uninstall`,
|
||||
|
||||
INVITES: `/${API_VERSION}/fleet/invites`,
|
||||
|
||||
|
|
@ -165,7 +167,7 @@ export default {
|
|||
SOFTWARE_PACKAGE_TOKEN: (id: number) =>
|
||||
`/${API_VERSION}/fleet/software/titles/${id}/package/token`,
|
||||
SOFTWARE_INSTALL_RESULTS: (uuid: string) =>
|
||||
`/${API_VERSION}/fleet/software/install/results/${uuid}`,
|
||||
`/${API_VERSION}/fleet/software/install/${uuid}/results`,
|
||||
SOFTWARE_PACKAGE_INSTALL: (id: number) =>
|
||||
`/${API_VERSION}/fleet/software/packages/${id}`,
|
||||
SOFTWARE_AVAILABLE_FOR_INSTALL: (id: number) =>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import installDeb from "../../pkg/file/scripts/install_deb.sh";
|
|||
* getInstallScript returns a string with a script to install the
|
||||
* provided software.
|
||||
* */
|
||||
const getInstallScript = (fileName: string): string => {
|
||||
const getDefaultInstallScript = (fileName: string): string => {
|
||||
const extension = fileName.split(".").pop();
|
||||
switch (extension) {
|
||||
case "pkg":
|
||||
|
|
@ -27,4 +27,4 @@ const getInstallScript = (fileName: string): string => {
|
|||
}
|
||||
};
|
||||
|
||||
export default getInstallScript;
|
||||
export default getDefaultInstallScript;
|
||||
|
|
|
|||
30
frontend/utilities/software_uninstall_scripts.ts
Normal file
30
frontend/utilities/software_uninstall_scripts.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
// @ts-ignore
|
||||
import uninstallPkg from "../../pkg/file/scripts/uninstall_pkg.sh";
|
||||
// @ts-ignore
|
||||
import uninstallMsi from "../../pkg/file/scripts/uninstall_msi.ps1";
|
||||
// @ts-ignore
|
||||
import uninstallExe from "../../pkg/file/scripts/uninstall_exe.ps1";
|
||||
// @ts-ignore
|
||||
import uninstallDeb from "../../pkg/file/scripts/uninstall_deb.sh";
|
||||
|
||||
/*
|
||||
* getUninstallScript returns a string with a script to uninstall the
|
||||
* provided software.
|
||||
* */
|
||||
const getDefaultUninstallScript = (fileName: string): string => {
|
||||
const extension = fileName.split(".").pop();
|
||||
switch (extension) {
|
||||
case "pkg":
|
||||
return uninstallPkg;
|
||||
case "msi":
|
||||
return uninstallMsi;
|
||||
case "deb":
|
||||
return uninstallDeb;
|
||||
case "exe":
|
||||
return uninstallExe;
|
||||
default:
|
||||
throw new Error(`unsupported file extension: ${extension}`);
|
||||
}
|
||||
};
|
||||
|
||||
export default getDefaultUninstallScript;
|
||||
58
go.mod
58
go.mod
|
|
@ -3,7 +3,7 @@ module github.com/fleetdm/fleet/v4
|
|||
go 1.23.1
|
||||
|
||||
require (
|
||||
cloud.google.com/go/pubsub v1.36.1
|
||||
cloud.google.com/go/pubsub v1.37.0
|
||||
fyne.io/systray v1.10.1-0.20240111184411-11c585fff98d
|
||||
github.com/AbGuthrie/goquery/v2 v2.0.1
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
|
|
@ -90,7 +90,7 @@ require (
|
|||
github.com/rs/zerolog v1.32.0
|
||||
github.com/russellhaering/goxmldsig v1.2.0
|
||||
github.com/saferwall/pe v1.5.2
|
||||
github.com/sassoftware/relic/v7 v7.6.2
|
||||
github.com/sassoftware/relic/v8 v8.0.1
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9
|
||||
github.com/sethvargo/go-password v0.3.0
|
||||
github.com/shirou/gopsutil/v3 v3.24.3
|
||||
|
|
@ -103,7 +103,7 @@ require (
|
|||
github.com/theupdateframework/go-tuf v0.5.2
|
||||
github.com/throttled/throttled/v2 v2.8.0
|
||||
github.com/tj/assert v0.0.3
|
||||
github.com/ulikunitz/xz v0.5.11
|
||||
github.com/ulikunitz/xz v0.5.12
|
||||
github.com/urfave/cli/v2 v2.23.5
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8
|
||||
github.com/ziutek/mymysql v1.5.4
|
||||
|
|
@ -126,7 +126,7 @@ require (
|
|||
golang.org/x/sys v0.21.0
|
||||
golang.org/x/text v0.16.0
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
|
||||
google.golang.org/api v0.169.0
|
||||
google.golang.org/api v0.178.0
|
||||
google.golang.org/grpc v1.64.1
|
||||
gopkg.in/guregu/null.v3 v3.5.0
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
|
|
@ -137,11 +137,13 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.112.1 // indirect
|
||||
cloud.google.com/go v0.112.2 // indirect
|
||||
cloud.google.com/go/auth v0.3.0 // indirect
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.6 // indirect
|
||||
cloud.google.com/go/kms v1.15.7 // indirect
|
||||
cloud.google.com/go/storage v1.38.0 // indirect
|
||||
cloud.google.com/go/iam v1.1.8 // indirect
|
||||
cloud.google.com/go/kms v1.15.9 // indirect
|
||||
cloud.google.com/go/storage v1.39.1 // indirect
|
||||
code.gitea.io/sdk/gitea v0.15.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
|
|
@ -167,7 +169,7 @@ require (
|
|||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/OneOfOne/xxhash v1.2.8 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.0.0 // indirect
|
||||
github.com/agnivade/levenshtein v1.1.1 // indirect
|
||||
github.com/akavel/rsrc v0.10.2 // indirect
|
||||
github.com/alecthomas/jsonschema v0.0.0-20211022214203-8b29eab41725 // indirect
|
||||
|
|
@ -175,20 +177,20 @@ require (
|
|||
github.com/apache/thrift v0.18.1 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/atc0005/go-teams-notify/v2 v2.6.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.27.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 // indirect
|
||||
github.com/aws/smithy-go v1.19.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 // indirect
|
||||
github.com/aws/smithy-go v1.20.2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/c-bata/go-prompt v0.2.3 // indirect
|
||||
github.com/caarlos0/ctrlc v1.0.0 // indirect
|
||||
|
|
@ -197,7 +199,7 @@ require (
|
|||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.7 // indirect
|
||||
github.com/cloudflare/circl v1.3.8 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
|
||||
|
|
@ -236,7 +238,7 @@ require (
|
|||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/wire v0.5.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 // indirect
|
||||
github.com/goreleaser/chglog v0.1.2 // indirect
|
||||
github.com/goreleaser/fileglob v1.2.0 // indirect
|
||||
github.com/gorilla/schema v1.4.1 // indirect
|
||||
|
|
@ -257,7 +259,7 @@ require (
|
|||
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/compress v1.17.8 // indirect
|
||||
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
|
|
@ -272,7 +274,7 @@ require (
|
|||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.10.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||
|
|
@ -321,7 +323,7 @@ require (
|
|||
golang.org/x/term v0.21.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
|
|
|
|||
116
go.sum
116
go.sum
|
|
@ -31,8 +31,12 @@ cloud.google.com/go v0.92.2/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
|
|||
cloud.google.com/go v0.92.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
|
||||
cloud.google.com/go v0.94.0/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
|
||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
||||
cloud.google.com/go v0.112.2 h1:ZaGT6LiG7dBzi6zNOvVZwacaXlmf3lRqnC4DQzqyRQw=
|
||||
cloud.google.com/go v0.112.2/go.mod h1:iEqjp//KquGIJV/m+Pk3xecgKNhV+ry+vVTsy4TbDms=
|
||||
cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs=
|
||||
cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4=
|
||||
cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
|
||||
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
|
||||
|
|
@ -45,19 +49,19 @@ cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7
|
|||
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
|
||||
cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
|
||||
cloud.google.com/go/firestore v1.5.0/go.mod h1:c4nNYR1qdq7eaZ+jSc5fonrQN2k3M7sWATcYTiakjEo=
|
||||
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
|
||||
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
|
||||
cloud.google.com/go/iam v1.1.8 h1:r7umDwhj+BQyz0ScZMp4QrGXjSTI3ZINnpgU2nlB/K0=
|
||||
cloud.google.com/go/iam v1.1.8/go.mod h1:GvE6lyMmfxXauzNq8NbgJbeVQNspG+tcdL/W8QO1+zE=
|
||||
cloud.google.com/go/kms v0.1.0/go.mod h1:8Qp8PCAypHg4FdmlyW1QRAv09BGQ9Uzh7JnmIZxPk+c=
|
||||
cloud.google.com/go/kms v1.15.7 h1:7caV9K3yIxvlQPAcaFffhlT7d1qpxjB1wHBtjWa13SM=
|
||||
cloud.google.com/go/kms v1.15.7/go.mod h1:ub54lbsa6tDkUwnu4W7Yt1aAIFLnspgh0kPGToDukeI=
|
||||
cloud.google.com/go/kms v1.15.9 h1:ouZjTxCqDNEdxWfaAAbRzG22s/2iewRw6JPARQL+0vc=
|
||||
cloud.google.com/go/kms v1.15.9/go.mod h1:5v/R/RRuBUVO+eJioGcqENr3syh8ZqNn1y1Wc9DjM+4=
|
||||
cloud.google.com/go/monitoring v0.1.0/go.mod h1:Hpm3XfzJv+UTiXzCG5Ffp0wijzHTC7Cv4eR7o3x/fEE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
|
||||
cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
|
||||
cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
|
||||
cloud.google.com/go/pubsub v1.16.0/go.mod h1:6A8EfoWZ/lUvCWStKGwAWauJZSiuV0Mkmu6WilK/TxQ=
|
||||
cloud.google.com/go/pubsub v1.36.1 h1:dfEPuGCHGbWUhaMCTHUFjfroILEkx55iUmKBZTP5f+Y=
|
||||
cloud.google.com/go/pubsub v1.36.1/go.mod h1:iYjCa9EzWOoBiTdd4ps7QoMtMln5NwaZQpK1hbRfBDE=
|
||||
cloud.google.com/go/pubsub v1.37.0 h1:0uEEfaB1VIJzabPpwpZf44zWAKAme3zwKKxHk7vJQxQ=
|
||||
cloud.google.com/go/pubsub v1.37.0/go.mod h1:YQOQr1uiUM092EXwKs56OPT650nwnawc+8/IjoUeGzQ=
|
||||
cloud.google.com/go/secretmanager v0.1.0/go.mod h1:3nGKHvnzDUVit7U0S9KAKJ4aOsO1xtwRG+7ey5LK1bM=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
|
||||
|
|
@ -65,8 +69,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||
cloud.google.com/go/storage v1.16.1/go.mod h1:LaNorbty3ehnU3rEjXSNV/NRgQA0O8Y+uh6bPe5UOk4=
|
||||
cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg=
|
||||
cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY=
|
||||
cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
|
||||
cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
|
||||
cloud.google.com/go/trace v0.1.0/go.mod h1:wxEwsoeRVPbeSkt7ZC9nWCgmoKQRAoySN7XHW2AmI7g=
|
||||
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||
code.gitea.io/sdk/gitea v0.15.0 h1:tsNhxDM/2N1Ohv1Xq5UWrht/esg0WmtRj4wsHVHriTg=
|
||||
|
|
@ -174,8 +178,8 @@ github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA
|
|||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=
|
||||
github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78=
|
||||
github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4=
|
||||
github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
|
||||
github.com/ProtonMail/gopenpgp/v2 v2.2.2 h1:u2m7xt+CZWj88qK1UUNBoXeJCFJwJCZ/Ff4ymGoxEXs=
|
||||
|
|
@ -243,45 +247,45 @@ github.com/aws/aws-sdk-go v1.40.34/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm
|
|||
github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q=
|
||||
github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.0/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1 h1:xAojnj+ktS95YZlDf0zxWBkbFtymPeDP+rvUQIH3uAU=
|
||||
github.com/aws/aws-sdk-go-v2 v1.24.1/go.mod h1:LNh45Br1YAkEKaAqvmE1m8FUx6a5b/V0oAKV7of29b4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.7.0/go.mod h1:w9+nMZ7soXCe5nT46Ri354SNhXDQ6v+V5wqDjnZE+GY=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6 h1:Z/7w9bUqlRI0FFQpetVuFYEsjzE3h7fpU6HuGmfPL/o=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.26.6/go.mod h1:uKU6cnDmYCvJ+pxO9S4cWDb2yWWIH5hra+32hVh1MI4=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.0/go.mod h1:dgGR+Qq7Wjcd4AOAW5Rf5Tnv3+x7ed6kETXyS9WCuAY=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16 h1:8q6Rliyv0aUFAVtzaldUEcS+T5gbadPbWdV1WcAddK8=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.16.16/go.mod h1:UHVZrdUsv63hPXFo1H7c5fEneoVo9UXiz36QG1GEPi0=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.5.0/go.mod h1:CpNzHK9VEFUCknu50kkB8z58AH2B5DvPP7ea1LHve/Y=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 h1:c5I5iH+DZcH3xOIMlz3/tCKJDaHFwYEmxvlh2fAcFo8=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11/go.mod h1:cRrYDYAMUohBJUtUnOhydaMHtiK/1NZ0Otc9lIb6O0Y=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 h1:vF+Zgd9s+H4vOXd5BMaPWykta2a6Ih0AKLq/X6NYKn4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10/go.mod h1:6BkRjejp/GR4411UGqkX8+wFMbFbqsUIimfK4XjOKR4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 h1:nYPe006ktcqUji8S2mqXf9c/7NdiKriOwMvWQHgYztw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10/go.mod h1:6UV4SZkVvmODfXKql4LCbaZUpF7HO2BX38FgBf9ZOLw=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.2/go.mod h1:BQV0agm+JEhqR+2RT5e1XTFIDcAAV0eW6z2trp+iduw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3 h1:n3GDfwqF2tzEkXlv5cuy4iy7LpKDtqDMcNLfZDu9rls=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.3/go.mod h1:6fQQgfuGmw8Al/3M2IgIllycxV7ZW7WCdVSqfBeUiCY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 h1:/b31bi3YVNlkzkBrm9LfpaKoaYZUxIAj4sHfOTmLfqw=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4/go.mod h1:2aGXHFmbInwgP9ZfpmdIfOELL79zhdNYNmReK8qDfdQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.0/go.mod h1:R1KK+vY8AfalhG1AOu5e35pOD2SdoPKQCFLTvnxiohk=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 h1:DBYTXwIGQSGs9w4jKm60F5dmCQ3EEruxdc0MFh+3EY4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10/go.mod h1:wohMUQiFdzo0NtxbBg0mSRGZ4vL3n0dKjLTINdcIino=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.5.0/go.mod h1:w7JuP9Oq1IKMFQPkNe3V6s9rOssXzOVEMNEqK1L1bao=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.27.9 h1:W9PbZAZAEcelhhjb7KuwUtf+Lbc+i7ByYJRuWLlnxyQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.27.9/go.mod h1:2tFmR7fQnOdQlM2ZCEPpFnBIQD1U8wmXmduBgZbOag0=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.0 h1:yl7wcqbisxPzknJVfWTLnK83McUvXba+pz2+tPbIUmQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/kms v1.31.0/go.mod h1:2snWQJQUKsbN66vAawJuOGX7dr37pfOq9hb0tZDGIqQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.6.0/go.mod h1:B+7C5UKdVq1ylkI/A6O8wcurFtaux0R1njePNPtKwoA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.10.0/go.mod h1:4dXS5YNqI3SNbetQ7X7vfsMlX6ZnboJA2dulBwJx7+g=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.4.0/go.mod h1:+1fpWnL96DL23aXPpMGbsmKe8jLTEfbjuQoA4WS1VaA=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7 h1:eajuO3nykDPdYicLlP3AGgOyVN3MOlFmZv7WGTuJPow=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.18.7/go.mod h1:+mJNDdF+qiUlNKNC3fxn74WWNN+sOiGOEImje+3ScPM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7 h1:QPMJf+Jw8E1l7zqhZmMlFw6w1NmfkfiSK8mS4zOx3BA=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.7/go.mod h1:ykf3COxYI0UJmxcfcxcVuz7b6uADi1FkiUz6Eb7AgM8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.7.0/go.mod h1:0qcSMCyASQPN2sk/1KQLQ2Fh6yq8wm0HSDAimPhzCoM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7 h1:NzO4Vrau795RkUdSHKEwiR01FaGzGOH1EETJ+5QHnm0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7/go.mod h1:6h2YuIoxaMSCFf5fi1EgZAwdfkGMgDY+DVfa61uLe4U=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6 h1:cwIxeBttqPN3qkaAjcEcsh8NYr8n2HZPkcKgPAi1phU=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.28.6/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
|
||||
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.19.0 h1:KWFKQV80DpP3vJrrA9sVAHQ5gc2z8i4EzrLhLlWXcBM=
|
||||
github.com/aws/smithy-go v1.19.0/go.mod h1:NukqUGpCZIILqqiV0NIjeFh24kd/FAa4beRb6nbIUPE=
|
||||
github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
|
||||
github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||
github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A=
|
||||
github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU=
|
||||
|
|
@ -336,8 +340,8 @@ github.com/clbanning/mxj v1.8.4 h1:HuhwZtbyvyOw+3Z1AowPkU87JkJUSv751ELWaiTpj8I=
|
|||
github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
|
||||
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
|
||||
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
|
||||
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
|
|
@ -668,8 +672,8 @@ github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksP
|
|||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4 h1:9gWcmF85Wvq4ryPFvGFaOgPIs1AQX0d0bcbGw4Z96qg=
|
||||
github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoFubselGIoBMCwI=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/goreleaser/chglog v0.1.2 h1:tdzAb/ILeMnphzI9zQ7Nkq+T8R9qyXli8GydD8plFRY=
|
||||
github.com/goreleaser/chglog v0.1.2/go.mod h1:tTZsFuSZK4epDXfjMkxzcGbrIOXprf0JFp47BjIr3B8=
|
||||
|
|
@ -805,8 +809,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||
github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
|
||||
github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab h1:KVR7cs+oPyy85i+8t1ZaNSy1bymCy5FuWyt51pdrXu4=
|
||||
github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY=
|
||||
github.com/kolide/launcher v1.0.12 h1:f2uT1kKYGIbj/WVsHDc10f7MIiwu8MpmgwaGaT7D09k=
|
||||
|
|
@ -944,8 +948,8 @@ github.com/open-policy-agent/opa v0.44.0/go.mod h1:YpJaFIk5pq89n/k72c1lVvfvR5uop
|
|||
github.com/opencensus-integrations/ocsql v0.1.1/go.mod h1:ozPYpNVBHZsX33jfoQPO5TlI5lqh0/3R36kirEqJKAM=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/oschwald/geoip2-golang v1.8.0 h1:KfjYB8ojCEn/QLqsDU0AzrJ3R5Qa9vFlx3z6SLNcKTs=
|
||||
github.com/oschwald/geoip2-golang v1.8.0/go.mod h1:R7bRvYjOeaoenAp9sKRS8GX5bJWcZ0laWO5+DauEktw=
|
||||
github.com/oschwald/maxminddb-golang v1.10.0 h1:Xp1u0ZhqkSuopaKmk1WwHtjF0H9Hd9181uj2MQ5Vndg=
|
||||
|
|
@ -1024,8 +1028,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
|
||||
github.com/saferwall/pe v1.5.2 h1:h5lLtLsyxGHQ9dN6cd8EfeLEBEo5gdqJpkuw4o4vTMY=
|
||||
github.com/saferwall/pe v1.5.2/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s=
|
||||
github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4=
|
||||
github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k=
|
||||
github.com/sassoftware/relic/v8 v8.0.1 h1:uYUoaoTQMs67up8/46NgrSxSftgfY4VWBusDVg56k7I=
|
||||
github.com/sassoftware/relic/v8 v8.0.1/go.mod h1:s/MwugRcovgYcNJNOyvLfqRHDX7iArHtFtUR9kEodz8=
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
|
||||
github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
|
|
@ -1142,8 +1146,8 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
|
|||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
|
||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
|
||||
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/urfave/cli/v2 v2.23.5 h1:xbrU7tAYviSpqeR3X4nEFWUdB/uDZ6DE+HxmRU7Xtyw=
|
||||
github.com/urfave/cli/v2 v2.23.5/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||
github.com/vartanbeno/go-reddit/v2 v2.0.0 h1:fxYMqx5lhbmJ3yYRN1nnQC/gecRB3xpUS2BbG7GLpsk=
|
||||
|
|
@ -1655,8 +1659,8 @@ google.golang.org/api v0.52.0/go.mod h1:Him/adpjt0sxtkWViy0b6xyKW/SD71CwdJ7HqJo7
|
|||
google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=
|
||||
google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=
|
||||
google.golang.org/api v0.169.0 h1:QwWPy71FgMWqJN/l6jVlFHUa29a7dcUy02I8o799nPY=
|
||||
google.golang.org/api v0.169.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
|
||||
google.golang.org/api v0.178.0 h1:yoW/QMI4bRVCHF+NWOTa4cL8MoWL3Jnuc7FlcFF91Ok=
|
||||
google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
|
|
@ -1726,8 +1730,8 @@ google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEc
|
|||
google.golang.org/genproto v0.0.0-20210825212027-de86158e7fda/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y=
|
||||
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s=
|
||||
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae h1:HjgkYCl6cWQEKSHkpUp4Q8VB74swzyBwTz1wtTzahm0=
|
||||
google.golang.org/genproto v0.0.0-20240506185236-b8a5c65736ae/go.mod h1:i4np6Wrjp8EujFAUn0CM0SH+iZhY1EbrfzEIJbFkHFM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
|
||||
|
|
|
|||
|
|
@ -33,9 +33,9 @@ func ExtractDebMetadata(r io.Reader) (*InstallerMetadata, error) {
|
|||
return nil, fmt.Errorf("failed to advance to next file in archive: %w", err)
|
||||
}
|
||||
|
||||
name := path.Clean(hdr.Name)
|
||||
if strings.HasPrefix(name, "control.tar") {
|
||||
ext := filepath.Ext(name)
|
||||
filename := path.Clean(hdr.Name)
|
||||
if strings.HasPrefix(filename, "control.tar") {
|
||||
ext := filepath.Ext(filename)
|
||||
if ext == ".tar" {
|
||||
ext = ""
|
||||
}
|
||||
|
|
@ -49,9 +49,10 @@ func ExtractDebMetadata(r io.Reader) (*InstallerMetadata, error) {
|
|||
return nil, fmt.Errorf("failed to read all content: %w", err)
|
||||
}
|
||||
return &InstallerMetadata{
|
||||
Name: name,
|
||||
Version: version,
|
||||
SHASum: h.Sum(nil),
|
||||
Name: name,
|
||||
Version: version,
|
||||
PackageIDs: []string{name},
|
||||
SHASum: h.Sum(nil),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ type InstallerMetadata struct {
|
|||
BundleIdentifier string
|
||||
SHASum []byte
|
||||
Extension string
|
||||
PackageIDs []string
|
||||
}
|
||||
|
||||
// ExtractInstallerMetadata extracts the software name and version from the
|
||||
|
|
|
|||
|
|
@ -61,3 +61,32 @@ func GetRemoveScript(extension string) string {
|
|||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed scripts/uninstall_exe.ps1
|
||||
var uninstallExeScript string
|
||||
|
||||
//go:embed scripts/uninstall_pkg.sh
|
||||
var uninstallPkgScript string
|
||||
|
||||
//go:embed scripts/uninstall_msi.ps1
|
||||
var uninstallMsiScript string
|
||||
|
||||
//go:embed scripts/uninstall_deb.sh
|
||||
var uninstallDebScript string
|
||||
|
||||
// GetUninstallScript returns a script that can be used to uninstall a
|
||||
// software item with the given extension.
|
||||
func GetUninstallScript(extension string) string {
|
||||
switch extension {
|
||||
case "msi":
|
||||
return uninstallMsiScript
|
||||
case "deb":
|
||||
return uninstallDebScript
|
||||
case "pkg":
|
||||
return uninstallPkgScript
|
||||
case "exe":
|
||||
return uninstallExeScript
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -19,35 +20,43 @@ func TestMain(m *testing.M) {
|
|||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
// Note: to update the goldens, run the tests with `-update`:
|
||||
// Note: to update the goldens, delete testdata/scripts/* and run the tests with `-update`:
|
||||
//
|
||||
// go test ./pkg/file/... -update
|
||||
func TestGetInstallAndRemoveScript(t *testing.T) {
|
||||
scriptsByType := map[string][2]string{
|
||||
t.Parallel()
|
||||
scriptsByType := map[string]map[string]string{
|
||||
"msi": {
|
||||
"./scripts/install_msi.ps1",
|
||||
"./scripts/remove_msi.ps1",
|
||||
"install": "./scripts/install_msi.ps1",
|
||||
"remove": "./scripts/remove_msi.ps1",
|
||||
"uninstall": "./scripts/uninstall_msi.ps1",
|
||||
},
|
||||
"pkg": {
|
||||
"./scripts/install_pkg.sh",
|
||||
"./scripts/remove_pkg.sh",
|
||||
"install": "./scripts/install_pkg.sh",
|
||||
"remove": "./scripts/remove_pkg.sh",
|
||||
"uninstall": "./scripts/uninstall_pkg.sh",
|
||||
},
|
||||
"deb": {
|
||||
"./scripts/install_deb.sh",
|
||||
"./scripts/remove_deb.sh",
|
||||
"install": "./scripts/install_deb.sh",
|
||||
"remove": "./scripts/remove_deb.sh",
|
||||
"uninstall": "./scripts/uninstall_deb.sh",
|
||||
},
|
||||
"exe": {
|
||||
"./scripts/install_exe.ps1",
|
||||
"./scripts/remove_exe.ps1",
|
||||
"install": "./scripts/install_exe.ps1",
|
||||
"remove": "./scripts/remove_exe.ps1",
|
||||
"uninstall": "./scripts/uninstall_exe.ps1",
|
||||
},
|
||||
}
|
||||
|
||||
for itype, scripts := range scriptsByType {
|
||||
gotScript := GetInstallScript(itype)
|
||||
assertGoldenMatches(t, scripts[0], gotScript, *update)
|
||||
assertGoldenMatches(t, scripts["install"], gotScript, *update)
|
||||
|
||||
gotScript = GetRemoveScript(itype)
|
||||
assertGoldenMatches(t, scripts[1], gotScript, *update)
|
||||
assertGoldenMatches(t, scripts["remove"], gotScript, *update)
|
||||
|
||||
gotScript = GetUninstallScript(itype)
|
||||
assertGoldenMatches(t, scripts["uninstall"], gotScript, *update)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,5 +76,5 @@ func assertGoldenMatches(t *testing.T, goldenFile string, actual string, update
|
|||
|
||||
content, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(content), actual)
|
||||
assert.Equal(t, string(content), actual)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import (
|
|||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/sassoftware/relic/v7/lib/comdoc"
|
||||
"github.com/sassoftware/relic/v8/lib/comdoc"
|
||||
)
|
||||
|
||||
func ExtractMSIMetadata(r io.Reader) (*InstallerMetadata, error) {
|
||||
|
|
@ -77,10 +77,12 @@ func ExtractMSIMetadata(r io.Reader) (*InstallerMetadata, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
// MSI installer product information properties: https://learn.microsoft.com/en-us/windows/win32/msi/property-reference#product-information-properties
|
||||
return &InstallerMetadata{
|
||||
Name: strings.TrimSpace(props["ProductName"]),
|
||||
Version: strings.TrimSpace(props["ProductVersion"]),
|
||||
SHASum: h.Sum(nil),
|
||||
Name: strings.TrimSpace(props["ProductName"]),
|
||||
Version: strings.TrimSpace(props["ProductVersion"]),
|
||||
PackageIDs: []string{strings.TrimSpace(props["ProductCode"])},
|
||||
SHASum: h.Sum(nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,10 +50,12 @@ func ExtractPEMetadata(r io.Reader) (*InstallerMetadata, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing PE version resources: %w", err)
|
||||
}
|
||||
name := strings.TrimSpace(v["ProductName"])
|
||||
return applySpecialCases(&InstallerMetadata{
|
||||
Name: strings.TrimSpace(v["ProductName"]),
|
||||
Version: strings.TrimSpace(v["ProductVersion"]),
|
||||
SHASum: h.Sum(nil),
|
||||
Name: name,
|
||||
Version: strings.TrimSpace(v["ProductVersion"]),
|
||||
PackageIDs: []string{name},
|
||||
SHASum: h.Sum(nil),
|
||||
}, v), nil
|
||||
}
|
||||
|
||||
|
|
|
|||
21
pkg/file/pe_test.go
Normal file
21
pkg/file/pe_test.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractPEMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
file, err := os.Open("testdata/software-installers/hello-world-installer.exe")
|
||||
require.NoError(t, err)
|
||||
meta, err := ExtractPEMetadata(file)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, meta)
|
||||
assert.Equal(t, "Hello world", meta.Name)
|
||||
assert.Equal(t, "1.0.0", meta.Version)
|
||||
assert.Equal(t, []string{"Hello world"}, meta.PackageIDs)
|
||||
}
|
||||
|
|
@ -1,16 +1,20 @@
|
|||
# Learn more about .exe install scripts: http://fleetdm.com/learn-more-about/exe-install-scripts
|
||||
|
||||
$exeFilePath = "${env:INSTALLER_PATH}"
|
||||
|
||||
# extract the name of the executable to use as the sub-directory name
|
||||
$exeName = [System.IO.Path]::GetFileName($exeFilePath)
|
||||
$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath)
|
||||
|
||||
$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir
|
||||
|
||||
# check if the directory does not exist, and create it if necessary
|
||||
if (-not (Test-Path -Path $destinationPath)) {
|
||||
New-Item -ItemType Directory -Path $destinationPath
|
||||
# Add argument to install silently
|
||||
# Argument to make install silent depends on installer,
|
||||
# each installer might use different argument (usually it's "/S" or "/s")
|
||||
$processOptions = @{
|
||||
FilePath = "$exeFilePath"
|
||||
ArgumentList = "/S"
|
||||
PassThru = $true
|
||||
Wait = $true
|
||||
}
|
||||
|
||||
# Start process and track exit code
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# copy the .exe file to the new sub-directory
|
||||
$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName
|
||||
Copy-Item -Path $exeFilePath -Destination $destinationExePath
|
||||
# Prints the exit code
|
||||
Write-Host "Install exit code: $exitCode"
|
||||
|
|
|
|||
4
pkg/file/scripts/uninstall_deb.sh
Normal file
4
pkg/file/scripts/uninstall_deb.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package_name=$PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product name that's extracted on upload
|
||||
apt remove "$package_name" -y
|
||||
17
pkg/file/scripts/uninstall_exe.ps1
Normal file
17
pkg/file/scripts/uninstall_exe.ps1
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Fleet extracts name from installer (EXE) and saves it to package ID variable
|
||||
$softwareName = $PACKAGE_ID
|
||||
|
||||
# Get the list of subkeys under the Uninstall registry path
|
||||
$uninstallKeys = Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall" | ForEach-Object { Get-ItemProperty $_.PSPath }
|
||||
|
||||
# Loop through each registry key to find the one containing "$softwareName" in DisplayName and run uninstall command from UninstallString
|
||||
foreach ($key in $uninstallKeys) {
|
||||
if ($key.DisplayName -like "*$softwareName*") {
|
||||
# Get the uninstall command
|
||||
$uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
|
||||
|
||||
# Run the uninstall command with arguments using the call operator &
|
||||
& $uninstallCommand
|
||||
break # Exit the loop once the software is found and uninstalled
|
||||
}
|
||||
}
|
||||
4
pkg/file/scripts/uninstall_msi.ps1
Normal file
4
pkg/file/scripts/uninstall_msi.ps1
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
$product_code = $PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product code that's extracted on upload
|
||||
msiexec /quiet /x $product_code
|
||||
21
pkg/file/scripts/uninstall_pkg.sh
Normal file
21
pkg/file/scripts/uninstall_pkg.sh
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Fleet extracts and saves package IDs.
|
||||
pkg_ids=$PACKAGE_ID
|
||||
|
||||
# Get all files associated with package and remove them
|
||||
for pkg_id in "${pkg_ids[@]}"
|
||||
do
|
||||
# Get volume and location of package
|
||||
volume=$(pkgutil --pkg-info "$pkg_id" | grep -i "volume" | awk '{for (i=2; i<NF; i++) printf $i " "; print $NF}')
|
||||
location=$(pkgutil --pkg-info "$pkg_id" | grep -i "location" | awk '{for (i=2; i<NF; i++) printf $i " "; print $NF}')
|
||||
# Check if this package id corresponds to a valid/installed package
|
||||
if [[ ! -z "$volume" && ! -z "$location" ]]; then
|
||||
# Remove individual files/directories belonging to package
|
||||
pkgutil --files "$pkg_id" | sed -e 's@^@'"$volume""$location"'/@' | tr '\n' '\0' | xargs -n 1 -0 rm -rf
|
||||
# Remove receipts
|
||||
pkgutil --forget "$pkg_id"
|
||||
else
|
||||
echo "WARNING: volume or location are empty for package ID $pkg_id"
|
||||
fi
|
||||
done
|
||||
10
pkg/file/testdata/distribution/test-zero-installkbytes.xml
vendored
Normal file
10
pkg/file/testdata/distribution/test-zero-installkbytes.xml
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||
<installer-gui-script minSpecVersion="2">
|
||||
<pkg-ref id="com.bozo.zeroinstallsize" installKBytes="0" packageIdentifier="com.bozo.zeroinstallsize.app">
|
||||
<bundle-version>
|
||||
<bundle CFBundleShortVersionString="1.2.3" CFBundleVersion="8.10.34.234040" id="com.bozo.zeroinstallsize" path="ZeroInstallSize.app"/>
|
||||
</bundle-version>
|
||||
</pkg-ref>
|
||||
<product id="com.bozo.zeroinstallsize" version="1.2.3"/>
|
||||
<title>ZeroInstallSize</title>
|
||||
</installer-gui-script>
|
||||
28
pkg/file/testdata/scripts/install_exe.ps1.golden
vendored
28
pkg/file/testdata/scripts/install_exe.ps1.golden
vendored
|
|
@ -1,16 +1,20 @@
|
|||
# Learn more about .exe install scripts: http://fleetdm.com/learn-more-about/exe-install-scripts
|
||||
|
||||
$exeFilePath = "${env:INSTALLER_PATH}"
|
||||
|
||||
# extract the name of the executable to use as the sub-directory name
|
||||
$exeName = [System.IO.Path]::GetFileName($exeFilePath)
|
||||
$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath)
|
||||
|
||||
$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir
|
||||
|
||||
# check if the directory does not exist, and create it if necessary
|
||||
if (-not (Test-Path -Path $destinationPath)) {
|
||||
New-Item -ItemType Directory -Path $destinationPath
|
||||
# Add argument to install silently
|
||||
# Argument to make install silent depends on installer,
|
||||
# each installer might use different argument (usually it's "/S" or "/s")
|
||||
$processOptions = @{
|
||||
FilePath = "$exeFilePath"
|
||||
ArgumentList = "/S"
|
||||
PassThru = $true
|
||||
Wait = $true
|
||||
}
|
||||
|
||||
# Start process and track exit code
|
||||
$process = Start-Process @processOptions
|
||||
$exitCode = $process.ExitCode
|
||||
|
||||
# copy the .exe file to the new sub-directory
|
||||
$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName
|
||||
Copy-Item -Path $exeFilePath -Destination $destinationExePath
|
||||
# Prints the exit code
|
||||
Write-Host "Install exit code: $exitCode"
|
||||
|
|
|
|||
4
pkg/file/testdata/scripts/uninstall_deb.sh.golden
vendored
Normal file
4
pkg/file/testdata/scripts/uninstall_deb.sh.golden
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package_name=$PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product name that's extracted on upload
|
||||
apt remove "$package_name" -y
|
||||
17
pkg/file/testdata/scripts/uninstall_exe.ps1.golden
vendored
Normal file
17
pkg/file/testdata/scripts/uninstall_exe.ps1.golden
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Fleet extracts name from installer (EXE) and saves it to package ID variable
|
||||
$softwareName = $PACKAGE_ID
|
||||
|
||||
# Get the list of subkeys under the Uninstall registry path
|
||||
$uninstallKeys = Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall" | ForEach-Object { Get-ItemProperty $_.PSPath }
|
||||
|
||||
# Loop through each registry key to find the one containing "$softwareName" in DisplayName and run uninstall command from UninstallString
|
||||
foreach ($key in $uninstallKeys) {
|
||||
if ($key.DisplayName -like "*$softwareName*") {
|
||||
# Get the uninstall command
|
||||
$uninstallCommand = if ($key.QuietUninstallString) { $key.QuietUninstallString } else { $key.UninstallString }
|
||||
|
||||
# Run the uninstall command with arguments using the call operator &
|
||||
& $uninstallCommand
|
||||
break # Exit the loop once the software is found and uninstalled
|
||||
}
|
||||
}
|
||||
4
pkg/file/testdata/scripts/uninstall_msi.ps1.golden
vendored
Normal file
4
pkg/file/testdata/scripts/uninstall_msi.ps1.golden
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
$product_code = $PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product code that's extracted on upload
|
||||
msiexec /quiet /x $product_code
|
||||
21
pkg/file/testdata/scripts/uninstall_pkg.sh.golden
vendored
Normal file
21
pkg/file/testdata/scripts/uninstall_pkg.sh.golden
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Fleet extracts and saves package IDs.
|
||||
pkg_ids=$PACKAGE_ID
|
||||
|
||||
# Get all files associated with package and remove them
|
||||
for pkg_id in "${pkg_ids[@]}"
|
||||
do
|
||||
# Get volume and location of package
|
||||
volume=$(pkgutil --pkg-info "$pkg_id" | grep -i "volume" | awk '{for (i=2; i<NF; i++) printf $i " "; print $NF}')
|
||||
location=$(pkgutil --pkg-info "$pkg_id" | grep -i "location" | awk '{for (i=2; i<NF; i++) printf $i " "; print $NF}')
|
||||
# Check if this package id corresponds to a valid/installed package
|
||||
if [[ ! -z "$volume" && ! -z "$location" ]]; then
|
||||
# Remove individual files/directories belonging to package
|
||||
pkgutil --files "$pkg_id" | sed -e 's@^@'"$volume""$location"'/@' | tr '\n' '\0' | xargs -n 1 -0 rm -rf
|
||||
# Remove receipts
|
||||
pkgutil --forget "$pkg_id"
|
||||
else
|
||||
echo "WARNING: volume or location are empty for package ID $pkg_id"
|
||||
fi
|
||||
done
|
||||
1
pkg/file/testdata/software-installers/README.md
vendored
Normal file
1
pkg/file/testdata/software-installers/README.md
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
- `hello-world-installer.exe` is an installer with a text file. It was created using [Inno Setup](https://jrsoftware.org/isinfo.php) on Windows.
|
||||
BIN
pkg/file/testdata/software-installers/hello-world-installer.exe
vendored
Executable file
BIN
pkg/file/testdata/software-installers/hello-world-installer.exe
vendored
Executable file
Binary file not shown.
|
|
@ -123,6 +123,7 @@ type distributionPkgRef struct {
|
|||
BundleVersions []distributionBundleVersion `xml:"bundle-version"`
|
||||
MustClose distributionMustClose `xml:"must-close"`
|
||||
PackageIdentifier string `xml:"packageIdentifier,attr"`
|
||||
InstallKBytes string `xml:"installKBytes,attr"`
|
||||
}
|
||||
|
||||
// distributionBundleVersion represents the bundle-version element
|
||||
|
|
@ -223,21 +224,55 @@ func parseDistributionFile(rawXML []byte) (*InstallerMetadata, error) {
|
|||
return nil, fmt.Errorf("unmarshal Distribution XML: %w", err)
|
||||
}
|
||||
|
||||
name, identifier, version := getDistributionInfo(&distXML)
|
||||
name, identifier, version, packageIDs := getDistributionInfo(&distXML)
|
||||
return &InstallerMetadata{
|
||||
Name: name,
|
||||
Version: version,
|
||||
BundleIdentifier: identifier,
|
||||
PackageIDs: packageIDs,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
// getDistributionInfo gets the name, bundle identifier and version of a PKG distribution file
|
||||
func getDistributionInfo(d *distributionXML) (name string, identifier string, version string) {
|
||||
func getDistributionInfo(d *distributionXML) (name string, identifier string, version string, packageIDs []string) {
|
||||
var appVersion string
|
||||
|
||||
// find the package ids that have an installation size
|
||||
var packageIDSet = make(map[string]struct{}, 1)
|
||||
for _, pkg := range d.PkgRefs {
|
||||
if pkg.InstallKBytes != "" && pkg.InstallKBytes != "0" {
|
||||
var id string
|
||||
if pkg.PackageIdentifier != "" {
|
||||
id = pkg.PackageIdentifier
|
||||
} else if pkg.ID != "" {
|
||||
id = pkg.ID
|
||||
}
|
||||
if id != "" {
|
||||
packageIDSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(packageIDSet) == 0 {
|
||||
// if we didn't find any package IDs with installation size, then grab all of them
|
||||
for _, pkg := range d.PkgRefs {
|
||||
var id string
|
||||
if pkg.PackageIdentifier != "" {
|
||||
id = pkg.PackageIdentifier
|
||||
} else if pkg.ID != "" {
|
||||
id = pkg.ID
|
||||
}
|
||||
if id != "" {
|
||||
packageIDSet[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
for id := range packageIDSet {
|
||||
packageIDs = append(packageIDs, id)
|
||||
}
|
||||
|
||||
out:
|
||||
// first, look in all the bundle versions for one that has a `path` attribute
|
||||
// look in all the bundle versions for one that has a `path` attribute
|
||||
// that is not nested, this is generally the case for packages that distribute
|
||||
// `.app` files, which are ultimately picked up as an installed app by osquery
|
||||
for _, pkg := range d.PkgRefs {
|
||||
|
|
@ -284,6 +319,11 @@ out:
|
|||
identifier = d.Product.ID
|
||||
}
|
||||
|
||||
// if package IDs are still empty, use the identifier as the package ID
|
||||
if len(packageIDs) == 0 && identifier != "" {
|
||||
packageIDs = append(packageIDs, identifier)
|
||||
}
|
||||
|
||||
// for the name, try to use the title and fallback to the bundle
|
||||
// identifier
|
||||
if name == "" && d.Title != "" {
|
||||
|
|
@ -296,7 +336,7 @@ out:
|
|||
// for the version, try to use the top-level product version, if not,
|
||||
// fallback to any version definition alongside the name or the first
|
||||
// version in a pkg-ref we find.
|
||||
if version == "" && d.Product.Version != "" {
|
||||
if d.Product.Version != "" {
|
||||
version = d.Product.Version
|
||||
}
|
||||
if version == "" && appVersion != "" {
|
||||
|
|
@ -310,7 +350,7 @@ out:
|
|||
}
|
||||
}
|
||||
|
||||
return name, identifier, version
|
||||
return name, identifier, version, packageIDs
|
||||
}
|
||||
|
||||
// isValidAppFilePath checks if the given input is a file name ending with .app
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
|
@ -52,113 +53,147 @@ func TestCheckPKGSignature(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestParseRealDistributionFiles(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := []struct {
|
||||
file string
|
||||
expectedName string
|
||||
expectedVersion string
|
||||
expectedBundleID string
|
||||
file string
|
||||
expectedName string
|
||||
expectedVersion string
|
||||
expectedBundleID string
|
||||
expectedPackageIDs []string
|
||||
}{
|
||||
{
|
||||
file: "distribution-1password.xml",
|
||||
expectedName: "1Password.app",
|
||||
expectedVersion: "8.10.34",
|
||||
expectedBundleID: "com.1password.1password",
|
||||
file: "distribution-1password.xml",
|
||||
expectedName: "1Password.app",
|
||||
expectedVersion: "8.10.34",
|
||||
expectedBundleID: "com.1password.1password",
|
||||
expectedPackageIDs: []string{"com.1password.1password"},
|
||||
},
|
||||
{
|
||||
file: "distribution-chrome.xml",
|
||||
expectedName: "Google Chrome.app",
|
||||
expectedVersion: "126.0.6478.62",
|
||||
expectedBundleID: "com.google.Chrome",
|
||||
file: "distribution-chrome.xml",
|
||||
expectedName: "Google Chrome.app",
|
||||
expectedVersion: "126.0.6478.62",
|
||||
expectedBundleID: "com.google.Chrome",
|
||||
expectedPackageIDs: []string{"com.google.Chrome"},
|
||||
},
|
||||
{
|
||||
file: "distribution-edge.xml",
|
||||
expectedName: "Microsoft Edge.app",
|
||||
expectedVersion: "126.0.2592.56",
|
||||
expectedBundleID: "com.microsoft.edgemac",
|
||||
file: "distribution-edge.xml",
|
||||
expectedName: "Microsoft Edge.app",
|
||||
expectedVersion: "126.0.2592.56",
|
||||
expectedBundleID: "com.microsoft.edgemac",
|
||||
expectedPackageIDs: []string{"com.microsoft.edgemac"},
|
||||
},
|
||||
{
|
||||
file: "distribution-firefox.xml",
|
||||
expectedName: "Firefox.app",
|
||||
expectedVersion: "99.0",
|
||||
expectedBundleID: "org.mozilla.firefox",
|
||||
file: "distribution-firefox.xml",
|
||||
expectedName: "Firefox.app",
|
||||
expectedVersion: "99.0",
|
||||
expectedBundleID: "org.mozilla.firefox",
|
||||
expectedPackageIDs: []string{"org.mozilla.firefox"},
|
||||
},
|
||||
{
|
||||
file: "distribution-fleet.xml",
|
||||
expectedName: "Fleet osquery",
|
||||
expectedVersion: "42.0.0",
|
||||
expectedBundleID: "com.fleetdm.orbit",
|
||||
file: "distribution-fleet.xml",
|
||||
expectedName: "Fleet osquery",
|
||||
expectedVersion: "42.0.0",
|
||||
expectedBundleID: "com.fleetdm.orbit",
|
||||
expectedPackageIDs: []string{"com.fleetdm.orbit.base.pkg"},
|
||||
},
|
||||
{
|
||||
file: "distribution-go.xml",
|
||||
expectedName: "Go",
|
||||
expectedVersion: "go1.22.4",
|
||||
expectedBundleID: "org.golang.go",
|
||||
file: "distribution-go.xml",
|
||||
expectedName: "Go",
|
||||
expectedVersion: "go1.22.4",
|
||||
expectedBundleID: "org.golang.go",
|
||||
expectedPackageIDs: []string{"org.golang.go"},
|
||||
},
|
||||
{
|
||||
file: "distribution-microsoft-teams.xml",
|
||||
expectedName: "Microsoft Teams.app",
|
||||
expectedVersion: "24124.1412.2911.3341",
|
||||
expectedBundleID: "com.microsoft.teams2",
|
||||
expectedPackageIDs: []string{"com.microsoft.teams2", "com.microsoft.package.Microsoft_AutoUpdate.app",
|
||||
"com.microsoft.MSTeamsAudioDevice"},
|
||||
},
|
||||
{
|
||||
file: "distribution-zoom.xml",
|
||||
expectedName: "zoom.us.app",
|
||||
expectedVersion: "6.0.11.35001",
|
||||
expectedBundleID: "us.zoom.xos",
|
||||
file: "distribution-zoom.xml",
|
||||
expectedName: "zoom.us.app",
|
||||
expectedVersion: "6.0.11.35001",
|
||||
expectedBundleID: "us.zoom.xos",
|
||||
expectedPackageIDs: []string{"us.zoom.pkg.videomeeting"},
|
||||
},
|
||||
{
|
||||
file: "distribution-acrobatreader.xml",
|
||||
expectedName: "Adobe Acrobat Reader.app",
|
||||
expectedVersion: "24.002.20857",
|
||||
expectedBundleID: "com.adobe.Reader",
|
||||
expectedPackageIDs: []string{"com.adobe.acrobat.DC.reader.app.pkg.MUI", "com.adobe.acrobat.DC.reader.appsupport.pkg.MUI",
|
||||
"com.adobe.acrobat.reader.DC.reader.app.pkg.MUI", "com.adobe.armdc.app.pkg"},
|
||||
},
|
||||
{
|
||||
file: "distribution-airtame.xml",
|
||||
expectedName: "Airtame.app",
|
||||
expectedVersion: "4.10.1",
|
||||
expectedBundleID: "com.airtame.airtame-application",
|
||||
file: "distribution-airtame.xml",
|
||||
expectedName: "Airtame.app",
|
||||
expectedVersion: "4.10.1",
|
||||
expectedBundleID: "com.airtame.airtame-application",
|
||||
expectedPackageIDs: []string{"com.airtame.airtame-application"},
|
||||
},
|
||||
{
|
||||
file: "distribution-boxdrive.xml",
|
||||
expectedName: "Box.app",
|
||||
expectedVersion: "2.38.173",
|
||||
expectedBundleID: "com.box.desktop",
|
||||
expectedPackageIDs: []string{"com.box.desktop.installer.desktop", "com.box.desktop.installer.local.appsupport",
|
||||
"com.box.desktop.installer.autoupdater", "com.box.desktop.installer.osxfuse"},
|
||||
},
|
||||
{
|
||||
file: "distribution-iriunwebcam.xml",
|
||||
expectedName: "IriunWebcam.app",
|
||||
expectedVersion: "2.8.8",
|
||||
expectedBundleID: "com.iriun.macwebcam",
|
||||
// Note: "com.iriun.pkg.multicam" is part of the installer package, but it is not actually installed by default.
|
||||
// We can't reliably determine which packages are installed by the installer, so we just list all of them.
|
||||
expectedPackageIDs: []string{"com.iriun.pkg.webcam.tmp", "com.iriun.pkg.multicam"},
|
||||
},
|
||||
{
|
||||
file: "distribution-microsoftexcel.xml",
|
||||
expectedName: "Microsoft Excel.app",
|
||||
expectedVersion: "16.86",
|
||||
expectedBundleID: "com.microsoft.Excel",
|
||||
expectedPackageIDs: []string{"com.microsoft.package.Microsoft_Excel.app", "com.microsoft.package.Microsoft_AutoUpdate.app",
|
||||
"com.microsoft.pkg.licensing"},
|
||||
},
|
||||
{
|
||||
file: "distribution-microsoftword.xml",
|
||||
expectedName: "Microsoft Word.app",
|
||||
expectedVersion: "16.86",
|
||||
expectedBundleID: "com.microsoft.Word",
|
||||
expectedPackageIDs: []string{"com.microsoft.package.Microsoft_Word.app", "com.microsoft.package.Microsoft_AutoUpdate.app",
|
||||
"com.microsoft.pkg.licensing"},
|
||||
},
|
||||
{
|
||||
file: "distribution-miscrosoftpowerpoint.xml",
|
||||
expectedName: "Microsoft PowerPoint.app",
|
||||
expectedVersion: "16.86",
|
||||
expectedBundleID: "com.microsoft.Powerpoint",
|
||||
expectedPackageIDs: []string{"com.microsoft.package.Microsoft_PowerPoint.app", "com.microsoft.package.Microsoft_AutoUpdate.app",
|
||||
"com.microsoft.pkg.licensing"},
|
||||
},
|
||||
{
|
||||
file: "distribution-ringcentral.xml",
|
||||
expectedName: "RingCentral.app",
|
||||
expectedVersion: "24.1.32.9774",
|
||||
expectedBundleID: "com.ringcentral.glip",
|
||||
file: "distribution-ringcentral.xml",
|
||||
expectedName: "RingCentral.app",
|
||||
expectedVersion: "24.1.32.9774",
|
||||
expectedBundleID: "com.ringcentral.glip",
|
||||
expectedPackageIDs: []string{"com.ringcentral.glip"},
|
||||
},
|
||||
{
|
||||
file: "distribution-zoom-full.xml",
|
||||
expectedName: "Zoom Workplace",
|
||||
expectedVersion: "6.1.1.36333",
|
||||
expectedBundleID: "us.zoom.xos",
|
||||
file: "distribution-zoom-full.xml",
|
||||
expectedName: "Zoom Workplace",
|
||||
expectedVersion: "6.1.1.36333",
|
||||
expectedBundleID: "us.zoom.xos",
|
||||
expectedPackageIDs: []string{"us.zoom.pkg.videomeeting"},
|
||||
},
|
||||
{
|
||||
file: "test-zero-installkbytes.xml",
|
||||
expectedName: "ZeroInstallSize.app",
|
||||
expectedVersion: "1.2.3",
|
||||
expectedBundleID: "com.bozo.zeroinstallsize",
|
||||
expectedPackageIDs: []string{"com.bozo.zeroinstallsize.app"},
|
||||
},
|
||||
}
|
||||
|
||||
|
|
@ -168,6 +203,7 @@ func TestParseRealDistributionFiles(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
metadata, err := parseDistributionFile(rawXML)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, tt.expectedPackageIDs, metadata.PackageIDs)
|
||||
require.Equal(t, tt.expectedName, metadata.Name)
|
||||
require.Equal(t, tt.expectedVersion, metadata.Version)
|
||||
require.Equal(t, tt.expectedBundleID, metadata.BundleIdentifier)
|
||||
|
|
|
|||
|
|
@ -677,7 +677,7 @@ allow {
|
|||
# Host software installs
|
||||
##
|
||||
|
||||
# Global admins and maintainers can write (install) software on hosts (not
|
||||
# Global admins and maintainers can write (install/uninstall) software on hosts (not
|
||||
# gitops as this is not something that relates to fleetctl apply).
|
||||
allow {
|
||||
object.type == "host_software_installer_result"
|
||||
|
|
@ -685,7 +685,7 @@ allow {
|
|||
action == write
|
||||
}
|
||||
|
||||
# Team admin and maintainers can write (install) software on hosts for their
|
||||
# Team admin and maintainers can write (install/uninstall) software on hosts for their
|
||||
# teams (not gitops as this is not something that relates to fleetctl apply).
|
||||
allow {
|
||||
object.type == "host_software_installer_result"
|
||||
|
|
@ -937,7 +937,7 @@ allow {
|
|||
action == write
|
||||
}
|
||||
|
||||
# Global admins, maintainers, observer_plus and observers can read scripts.
|
||||
# Global admins, maintainers, observer_plus and observers can read script results, including software uninstall results.
|
||||
allow {
|
||||
object.type == "host_script_result"
|
||||
subject.global_role == [admin, maintainer, observer, observer_plus][_]
|
||||
|
|
@ -953,7 +953,7 @@ allow {
|
|||
action == write
|
||||
}
|
||||
|
||||
# Team admins, maintainers, observer_plus and observers can read scripts for their teams.
|
||||
# Team admins, maintainers, observer_plus and observers can read script results for their teams, including software uninstall results.
|
||||
allow {
|
||||
object.type == "host_script_result"
|
||||
not is_null(object.team_id)
|
||||
|
|
|
|||
|
|
@ -242,15 +242,22 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
`SELECT
|
||||
COUNT(*) c
|
||||
FROM host_script_results hsr
|
||||
LEFT OUTER JOIN
|
||||
host_software_installs hsi ON hsi.execution_id = hsr.execution_id
|
||||
WHERE hsr.host_id = :host_id AND
|
||||
exit_code IS NULL AND
|
||||
(sync_request = 0 OR created_at >= DATE_SUB(NOW(), INTERVAL :max_wait_time SECOND))`,
|
||||
hsi.execution_id IS NULL AND
|
||||
(sync_request = 0 OR hsr.created_at >= DATE_SUB(NOW(), INTERVAL :max_wait_time SECOND))`,
|
||||
`SELECT
|
||||
COUNT(*) c
|
||||
FROM host_software_installs hsi
|
||||
WHERE hsi.host_id = :host_id AND
|
||||
pre_install_query_output IS NULL AND
|
||||
install_script_exit_code IS NULL`,
|
||||
hsi.status = :software_status_install_pending`,
|
||||
`SELECT
|
||||
COUNT(*) c
|
||||
FROM host_software_installs hsi
|
||||
WHERE hsi.host_id = :host_id AND
|
||||
hsi.status = :software_status_uninstall_pending`,
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) c
|
||||
|
|
@ -265,8 +272,10 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
|
||||
seconds := int(scripts.MaxServerWaitTime.Seconds())
|
||||
countStmt, args, err := sqlx.Named(countStmt, map[string]any{
|
||||
"host_id": hostID,
|
||||
"max_wait_time": seconds,
|
||||
"host_id": hostID,
|
||||
"max_wait_time": seconds,
|
||||
"software_status_install_pending": fleet.SoftwareInstallPending,
|
||||
"software_status_uninstall_pending": fleet.SoftwareUninstallPending,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "build count query from named args")
|
||||
|
|
@ -305,13 +314,16 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
host_display_names hdn ON hdn.host_id = hsr.host_id
|
||||
LEFT OUTER JOIN
|
||||
scripts scr ON scr.id = hsr.script_id
|
||||
LEFT OUTER JOIN
|
||||
host_software_installs hsi ON hsi.execution_id = hsr.execution_id
|
||||
WHERE
|
||||
hsr.host_id = :host_id AND
|
||||
hsr.exit_code IS NULL AND
|
||||
(
|
||||
hsr.sync_request = 0 OR
|
||||
hsr.created_at >= DATE_SUB(NOW(), INTERVAL :max_wait_time SECOND)
|
||||
)
|
||||
) AND
|
||||
hsi.execution_id IS NULL
|
||||
`,
|
||||
// list pending software installs
|
||||
fmt.Sprintf(`SELECT
|
||||
|
|
@ -330,7 +342,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
'software_title', COALESCE(st.name, ''),
|
||||
'software_package', si.filename,
|
||||
'install_uuid', hsi.execution_id,
|
||||
'status', CAST(%s AS CHAR),
|
||||
'status', CAST(hsi.status AS CHAR),
|
||||
'self_service', si.self_service IS TRUE
|
||||
) as details
|
||||
FROM
|
||||
|
|
@ -347,9 +359,42 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint
|
|||
host_display_names hdn ON hdn.host_id = hsi.host_id
|
||||
WHERE
|
||||
hsi.host_id = :host_id AND
|
||||
hsi.pre_install_query_output IS NULL AND
|
||||
hsi.install_script_exit_code IS NULL
|
||||
`, softwareInstallerHostStatusNamedQuery("hsi", "")),
|
||||
hsi.status = :software_status_install_pending
|
||||
`),
|
||||
// list pending software uninstalls
|
||||
fmt.Sprintf(`SELECT
|
||||
hsi.execution_id as uuid,
|
||||
-- policies with automatic installers generate a host_software_installs with (user_id=NULL,self_service=0),
|
||||
-- thus the user_id for the upcoming activity needs to be the user that uploaded the software installer.
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.name, u.name) AS name,
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.id, u.id) as user_id,
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.gravatar_url, u.gravatar_url) as gravatar_url,
|
||||
IF(hsi.user_id IS NULL AND NOT hsi.self_service, u2.email, u.email) AS user_email,
|
||||
:uninstalled_software_type as activity_type,
|
||||
hsi.created_at as created_at,
|
||||
JSON_OBJECT(
|
||||
'host_id', hsi.host_id,
|
||||
'host_display_name', COALESCE(hdn.display_name, ''),
|
||||
'software_title', COALESCE(st.name, ''),
|
||||
'script_execution_id', hsi.execution_id,
|
||||
'status', CAST(hsi.status AS CHAR)
|
||||
) as details
|
||||
FROM
|
||||
host_software_installs hsi
|
||||
INNER JOIN
|
||||
software_installers si ON si.id = hsi.software_installer_id
|
||||
LEFT OUTER JOIN
|
||||
software_titles st ON st.id = si.title_id
|
||||
LEFT OUTER JOIN
|
||||
users u ON u.id = hsi.user_id
|
||||
LEFT OUTER JOIN
|
||||
users u2 ON u2.id = si.user_id
|
||||
LEFT OUTER JOIN
|
||||
host_display_names hdn ON hdn.host_id = hsi.host_id
|
||||
WHERE
|
||||
hsi.host_id = :host_id AND
|
||||
hsi.status = :software_status_uninstall_pending
|
||||
`),
|
||||
`
|
||||
SELECT
|
||||
hvsi.command_uuid AS uuid,
|
||||
|
|
@ -367,7 +412,7 @@ SELECT
|
|||
'command_uuid', hvsi.command_uuid,
|
||||
'self_service', hvsi.self_service IS TRUE,
|
||||
-- status is always pending because only pending MDM commands are upcoming.
|
||||
'status', :software_status_pending
|
||||
'status', :software_status_install_pending
|
||||
) AS details
|
||||
FROM
|
||||
host_vpp_software_installs hvsi
|
||||
|
|
@ -399,14 +444,14 @@ WHERE
|
|||
details
|
||||
FROM ( ` + strings.Join(listStmts, " UNION ALL ") + ` ) AS upcoming `
|
||||
listStmt, args, err = sqlx.Named(listStmt, map[string]any{
|
||||
"host_id": hostID,
|
||||
"ran_script_type": fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
"installed_software_type": fleet.ActivityTypeInstalledSoftware{}.ActivityName(),
|
||||
"installed_app_store_app_type": fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
|
||||
"max_wait_time": seconds,
|
||||
"software_status_failed": string(fleet.SoftwareInstallerFailed),
|
||||
"software_status_installed": string(fleet.SoftwareInstallerInstalled),
|
||||
"software_status_pending": string(fleet.SoftwareInstallerPending),
|
||||
"host_id": hostID,
|
||||
"ran_script_type": fleet.ActivityTypeRanScript{}.ActivityName(),
|
||||
"installed_software_type": fleet.ActivityTypeInstalledSoftware{}.ActivityName(),
|
||||
"uninstalled_software_type": fleet.ActivityTypeUninstalledSoftware{}.ActivityName(),
|
||||
"installed_app_store_app_type": fleet.ActivityInstalledAppStoreApp{}.ActivityName(),
|
||||
"max_wait_time": seconds,
|
||||
"software_status_install_pending": fleet.SoftwareInstallPending,
|
||||
"software_status_uninstall_pending": fleet.SoftwareUninstallPending,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "build list query from named args")
|
||||
|
|
|
|||
|
|
@ -508,7 +508,8 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) {
|
|||
h2A := hsr.ExecutionID
|
||||
hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h2.ID, ScriptContents: "F", UserID: &u.ID})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{HostID: h2.ID, ExecutionID: hsr.ExecutionID, Output: "ok", ExitCode: 0})
|
||||
_, _, err = ds.SetHostScriptExecutionResult(ctx,
|
||||
&fleet.HostScriptResultPayload{HostID: h2.ID, ExecutionID: hsr.ExecutionID, Output: "ok", ExitCode: 0})
|
||||
require.NoError(t, err)
|
||||
h2F := hsr.ExecutionID
|
||||
// add a pending software install request for h2
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
)
|
||||
|
||||
func init() {
|
||||
MigrationClient.AddMigration(Up_20240905200000, Down_20240905200000)
|
||||
}
|
||||
|
||||
const placeholderUninstallScript = "# This script will be automatically updated within the next hour\nexit 1"
|
||||
const placeholderUninstallScriptWindows = "# This script will be automatically updated within the next hour\nExit 1"
|
||||
|
||||
func Up_20240905200000(tx *sql.Tx) error {
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE software_installers
|
||||
ADD COLUMN package_ids TEXT COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
ADD COLUMN uninstall_script_content_id int unsigned NOT NULL,
|
||||
MODIFY COLUMN uploaded_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6)
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to alter software_installers: %w", err)
|
||||
}
|
||||
|
||||
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
|
||||
|
||||
// Add dummy uninstall scripts if needed -- these will be updated later by a cron job
|
||||
var result []int
|
||||
if err := txx.Select(&result, `SELECT 1 FROM software_installers WHERE platform IN ('linux', 'darwin')`); err != nil {
|
||||
return fmt.Errorf("failed to check software installers for linux or darwin: %w", err)
|
||||
}
|
||||
if len(result) > 0 {
|
||||
linuxScriptID, err := getOrInsertScript(txx, placeholderUninstallScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Update software installers with the scripts
|
||||
if _, err := tx.Exec(`UPDATE software_installers SET uninstall_script_content_id = ? WHERE platform IN ('linux', 'darwin')`,
|
||||
linuxScriptID); err != nil {
|
||||
return fmt.Errorf("failed to update software installers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := txx.Select(&result, `SELECT 1 FROM software_installers WHERE platform IN ('windows')`); err != nil {
|
||||
return fmt.Errorf("failed to check software installers for windows: %w", err)
|
||||
}
|
||||
if len(result) > 0 {
|
||||
windowsScriptID, err := getOrInsertScript(txx, placeholderUninstallScriptWindows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Update software installers with the scripts
|
||||
if _, err := tx.Exec(`UPDATE software_installers SET uninstall_script_content_id = ? WHERE platform IN ('windows')`,
|
||||
windowsScriptID); err != nil {
|
||||
return fmt.Errorf("failed to update windows software installers: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Add foreign key
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE software_installers
|
||||
ADD CONSTRAINT fk_uninstall_script_content_id
|
||||
FOREIGN KEY (uninstall_script_content_id)
|
||||
REFERENCES script_contents(id)
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE`); err != nil {
|
||||
return fmt.Errorf("failed to add foreign key to software_installers: %w", err)
|
||||
}
|
||||
|
||||
if _, err := tx.Exec(`
|
||||
ALTER TABLE host_software_installs
|
||||
ADD COLUMN uninstall_script_output TEXT COLLATE utf8mb4_unicode_ci,
|
||||
ADD COLUMN uninstall_script_exit_code INT DEFAULT NULL,
|
||||
ADD COLUMN uninstall TINYINT UNSIGNED NOT NULL DEFAULT 0,
|
||||
ADD COLUMN status ENUM('pending_install', 'failed_install', 'installed', 'pending_uninstall', 'failed_uninstall')
|
||||
GENERATED ALWAYS AS (
|
||||
CASE
|
||||
WHEN removed = 1 THEN NULL
|
||||
|
||||
WHEN post_install_script_exit_code IS NOT NULL AND
|
||||
post_install_script_exit_code = 0 THEN 'installed'
|
||||
|
||||
WHEN post_install_script_exit_code IS NOT NULL AND
|
||||
post_install_script_exit_code != 0 THEN 'failed_install'
|
||||
|
||||
WHEN install_script_exit_code IS NOT NULL AND
|
||||
install_script_exit_code = 0 THEN 'installed'
|
||||
|
||||
WHEN install_script_exit_code IS NOT NULL AND
|
||||
install_script_exit_code != 0 THEN 'failed_install'
|
||||
|
||||
WHEN pre_install_query_output IS NOT NULL AND
|
||||
pre_install_query_output = '' THEN 'failed_install'
|
||||
|
||||
WHEN host_id IS NOT NULL AND uninstall = 0 THEN 'pending_install'
|
||||
|
||||
WHEN uninstall_script_exit_code IS NOT NULL AND
|
||||
uninstall_script_exit_code != 0 THEN 'failed_uninstall'
|
||||
|
||||
WHEN uninstall_script_exit_code IS NOT NULL AND
|
||||
uninstall_script_exit_code = 0 THEN NULL -- available for install again
|
||||
|
||||
WHEN host_id IS NOT NULL AND uninstall = 1 THEN 'pending_uninstall'
|
||||
|
||||
ELSE NULL -- not installed from Fleet installer or successfully uninstalled
|
||||
END
|
||||
) STORED NULL,
|
||||
MODIFY COLUMN created_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||||
MODIFY COLUMN updated_at TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||||
MODIFY COLUMN host_deleted_at TIMESTAMP(6) NULL DEFAULT NULL
|
||||
`); err != nil {
|
||||
return fmt.Errorf("failed to alter host_software_installs: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getOrInsertScript(txx sqlx.Tx, script string) (int64, error) {
|
||||
var ids []int64
|
||||
// check is script already exists
|
||||
csum := md5ChecksumScriptContent(script)
|
||||
if err := txx.Select(&ids, `SELECT id FROM script_contents WHERE md5_checksum = UNHEX(?)`, csum); err != nil {
|
||||
return 0, fmt.Errorf("failed to find script contents: %w", err)
|
||||
}
|
||||
var scriptID int64
|
||||
if len(ids) > 0 {
|
||||
scriptID = ids[0]
|
||||
} else {
|
||||
// create new script
|
||||
var result sql.Result
|
||||
var err error
|
||||
if result, err = txx.Exec(`INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(?), ?)`, csum,
|
||||
script); err != nil {
|
||||
return 0, fmt.Errorf("failed to insert script contents: %w", err)
|
||||
}
|
||||
scriptID, _ = result.LastInsertId()
|
||||
}
|
||||
return scriptID, nil
|
||||
}
|
||||
|
||||
func Down_20240905200000(_ *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
package tables
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/jmoiron/sqlx/reflectx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUp_20240905200000(t *testing.T) {
|
||||
db := applyUpToPrev(t)
|
||||
|
||||
// Create host
|
||||
insertHostStmt := `
|
||||
INSERT INTO hosts (
|
||||
hostname, uuid, platform, osquery_version, os_version, build, platform_like, code_name,
|
||||
cpu_type, cpu_subtype, cpu_brand, hardware_vendor, hardware_model, hardware_version,
|
||||
hardware_serial, computer_name, team_id
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
hostName := "Dummy Hostname"
|
||||
hostUUID := "12345678-1234-1234-1234-123456789012"
|
||||
hostPlatform := "darwin"
|
||||
osqueryVer := "5.9.1"
|
||||
osVersion := "Windows 10"
|
||||
buildVersion := "10.0.19042.1234"
|
||||
platformLike := "apple"
|
||||
codeName := "20H2"
|
||||
cpuType := "x86_64"
|
||||
cpuSubtype := "x86_64"
|
||||
cpuBrand := "Intel"
|
||||
hwVendor := "Dell Inc."
|
||||
hwModel := "OptiPlex 7090"
|
||||
hwVersion := "1.0"
|
||||
hwSerial := "ABCDEFGHIJ"
|
||||
computerName := "DESKTOP-TEST"
|
||||
|
||||
hostID1 := execNoErrLastID(t, db, insertHostStmt, hostName, hostUUID, hostPlatform, osqueryVer,
|
||||
osVersion, buildVersion, platformLike, codeName, cpuType, cpuSubtype, cpuBrand, hwVendor, hwModel, hwVersion, hwSerial,
|
||||
computerName, nil)
|
||||
|
||||
dataStmts := `
|
||||
INSERT INTO script_contents (id, md5_checksum, contents) VALUES
|
||||
(1, 'checksum', 'script content');
|
||||
|
||||
INSERT INTO software_titles (id, name, source, browser) VALUES
|
||||
(1, 'Foo.app', 'apps', ''),
|
||||
(2, 'Go', 'deb_packages', ''),
|
||||
(3, 'Microsoft Teams.exe', 'programs', '');
|
||||
|
||||
INSERT INTO software_installers
|
||||
(id, title_id, filename, version, platform, install_script_content_id, storage_id)
|
||||
VALUES
|
||||
(1, 1, 'foo-installer.pkg', '1.1', 'darwin', 1, 'storage-id'),
|
||||
(2, 2, 'go-installer.deb', '2.2', 'linux', 1, 'storage-id'),
|
||||
(3, 3, 'teams-installer.exe', '3.3', 'windows', 1, 'storage-id');
|
||||
`
|
||||
_, err := db.Exec(dataStmts)
|
||||
require.NoError(t, err)
|
||||
|
||||
tx, err := db.Begin()
|
||||
require.NoError(t, err)
|
||||
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)}
|
||||
scriptID, err := getOrInsertScript(txx, placeholderUninstallScript)
|
||||
require.NoError(t, err)
|
||||
err = tx.Commit()
|
||||
require.NoError(t, err)
|
||||
|
||||
hsiStmt := `
|
||||
INSERT INTO host_software_installs (
|
||||
host_id,
|
||||
execution_id,
|
||||
software_installer_id,
|
||||
install_script_exit_code
|
||||
) VALUES (?, ?, ?, ?)`
|
||||
hsi1 := execNoErrLastID(t, db, hsiStmt, hostID1, "execution-id1", 1, 0)
|
||||
|
||||
// Apply current migration.
|
||||
applyNext(t, db)
|
||||
|
||||
var scriptIDs []int64
|
||||
err = db.Select(&scriptIDs, "SELECT uninstall_script_content_id FROM software_installers WHERE id IN (1, 2)")
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, []int64{scriptID, scriptID}, scriptIDs)
|
||||
|
||||
var windowsScript string
|
||||
err = db.Get(&windowsScript, `
|
||||
SELECT contents FROM script_contents sc
|
||||
INNER JOIN software_installers si ON sc.id = si.uninstall_script_content_id
|
||||
WHERE si.id = 3`)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, placeholderUninstallScriptWindows, windowsScript)
|
||||
|
||||
var status string
|
||||
err = db.Get(&status, "SELECT status FROM host_software_installs WHERE id = ?", hsi1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "installed", status)
|
||||
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -80,7 +80,8 @@ func truncateScriptResult(output string) string {
|
|||
return output
|
||||
}
|
||||
|
||||
func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *fleet.HostScriptResultPayload) (*fleet.HostScriptResult, error) {
|
||||
func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *fleet.HostScriptResultPayload) (*fleet.HostScriptResult,
|
||||
string, error) {
|
||||
const resultExistsStmt = `
|
||||
SELECT
|
||||
1
|
||||
|
|
@ -105,20 +106,27 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
|
|||
const hostMDMActionsStmt = `
|
||||
SELECT
|
||||
CASE
|
||||
WHEN lock_ref = ? THEN 'lock_ref'
|
||||
WHEN unlock_ref = ? THEN 'unlock_ref'
|
||||
WHEN wipe_ref = ? THEN 'wipe_ref'
|
||||
WHEN lock_ref = :execution_id THEN 'lock_ref'
|
||||
WHEN unlock_ref = :execution_id THEN 'unlock_ref'
|
||||
WHEN wipe_ref = :execution_id THEN 'wipe_ref'
|
||||
ELSE ''
|
||||
END AS ref_col
|
||||
END AS action
|
||||
FROM
|
||||
host_mdm_actions
|
||||
WHERE
|
||||
host_id = ?
|
||||
host_id = :host_id
|
||||
UNION
|
||||
SELECT 'uninstall' AS action
|
||||
FROM
|
||||
host_software_installs
|
||||
WHERE
|
||||
execution_id = :execution_id AND host_id = :host_id
|
||||
`
|
||||
|
||||
output := truncateScriptResult(result.Output)
|
||||
|
||||
var hsr *fleet.HostScriptResult
|
||||
var action string
|
||||
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
var resultExists bool
|
||||
err := sqlx.GetContext(ctx, tx, &resultExists, resultExistsStmt, result.HostID, result.ExecutionID)
|
||||
|
|
@ -154,15 +162,31 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
|
|||
return ctxerr.Wrap(ctx, err, "load updated host script result")
|
||||
}
|
||||
|
||||
// look up if that script was a lock/unlock/wipe script for that host,
|
||||
// look up if that script was a lock/unlock/wipe/uninstall script for that host,
|
||||
// and if so update the host_mdm_actions table accordingly.
|
||||
var refCol string
|
||||
err = sqlx.GetContext(ctx, tx, &refCol, hostMDMActionsStmt, result.ExecutionID, result.ExecutionID, result.ExecutionID, result.HostID)
|
||||
namedArgs := map[string]any{
|
||||
"host_id": result.HostID,
|
||||
"execution_id": result.ExecutionID,
|
||||
}
|
||||
stmt, args, err := sqlx.Named(hostMDMActionsStmt, namedArgs)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "build named query for host mdm actions")
|
||||
}
|
||||
err = sqlx.GetContext(ctx, tx, &action, stmt, args...)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) { // ignore ErrNoRows, refCol will be empty
|
||||
return ctxerr.Wrap(ctx, err, "lookup host script corresponding mdm action")
|
||||
}
|
||||
if refCol != "" {
|
||||
err = updateHostLockWipeStatusFromResult(ctx, tx, result.HostID, refCol, result.ExitCode == 0)
|
||||
|
||||
switch action {
|
||||
case "":
|
||||
// do nothing
|
||||
case "uninstall":
|
||||
err = updateUninstallStatusFromResult(ctx, tx, result.HostID, result.ExecutionID, result.ExitCode)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update host uninstall action based on script result")
|
||||
}
|
||||
default: // lock/unlock/wipe
|
||||
err = updateHostLockWipeStatusFromResult(ctx, tx, result.HostID, action, result.ExitCode == 0)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update host mdm action based on script result")
|
||||
}
|
||||
|
|
@ -171,9 +195,9 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
|
|||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return hsr, nil
|
||||
return hsr, action, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListPendingHostScriptExecutions(ctx context.Context, hostID uint) ([]*fleet.HostScriptResult, error) {
|
||||
|
|
@ -392,6 +416,25 @@ WHERE
|
|||
return contents, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetAnyScriptContents(ctx context.Context, id uint) ([]byte, error) {
|
||||
const getStmt = `
|
||||
SELECT
|
||||
sc.contents
|
||||
FROM
|
||||
script_contents sc
|
||||
WHERE
|
||||
sc.id = ?
|
||||
`
|
||||
var contents []byte
|
||||
if err := sqlx.GetContext(ctx, ds.reader(ctx), &contents, getStmt, id); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, notFound("Script").WithID(id)
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, err, "get any script contents")
|
||||
}
|
||||
return contents, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) DeleteScript(ctx context.Context, id uint) error {
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, `DELETE FROM scripts WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
|
|
@ -1100,6 +1143,16 @@ func updateHostLockWipeStatusFromResult(ctx context.Context, tx sqlx.ExtContext,
|
|||
return ctxerr.Wrap(ctx, err, "update host lock/wipe status from result")
|
||||
}
|
||||
|
||||
func updateUninstallStatusFromResult(ctx context.Context, tx sqlx.ExtContext, hostID uint, executionID string, exitCode int) error {
|
||||
stmt := `
|
||||
UPDATE host_software_installs SET uninstall_script_exit_code = ? WHERE execution_id = ? AND host_id = ?
|
||||
`
|
||||
if _, err := tx.ExecContext(ctx, stmt, exitCode, executionID, hostID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update uninstall status from result")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) CleanupUnusedScriptContents(ctx context.Context) error {
|
||||
deleteStmt := `
|
||||
DELETE FROM
|
||||
|
|
@ -1111,7 +1164,7 @@ WHERE
|
|||
SELECT 1 FROM scripts WHERE script_content_id = script_contents.id)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM software_installers si
|
||||
WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id)
|
||||
WHERE script_contents.id IN (si.install_script_content_id, si.post_install_script_content_id, si.uninstall_script_content_id)
|
||||
)
|
||||
`
|
||||
_, err := ds.writer(ctx).ExecContext(ctx, deleteStmt)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func TestScripts(t *testing.T) {
|
|||
{"TestLockUnlockManually", testLockUnlockManually},
|
||||
{"TestInsertScriptContents", testInsertScriptContents},
|
||||
{"TestCleanupUnusedScriptContents", testCleanupUnusedScriptContents},
|
||||
{"TestGetAnyScriptContents", testGetAnyScriptContents},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -87,7 +88,7 @@ func testHostScriptResult(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, createdScript.ID, pending[0].ID)
|
||||
|
||||
// record a result for this execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
hsr, action, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: 1,
|
||||
ExecutionID: createdScript.ExecutionID,
|
||||
Output: "foo",
|
||||
|
|
@ -96,9 +97,11 @@ func testHostScriptResult(t *testing.T, ds *Datastore) {
|
|||
Timeout: 300,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, action)
|
||||
assert.NotNil(t, hsr)
|
||||
|
||||
// record a duplicate result for this execution, will be ignored
|
||||
hsr, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
hsr, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: 1,
|
||||
ExecutionID: createdScript.ExecutionID,
|
||||
Output: "foobarbaz",
|
||||
|
|
@ -163,7 +166,7 @@ func testHostScriptResult(t *testing.T, ds *Datastore) {
|
|||
strings.Repeat("j", 1000) +
|
||||
strings.Repeat("k", 1000)
|
||||
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, _, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: 1,
|
||||
ExecutionID: createdScript.ExecutionID,
|
||||
Output: largeOutput,
|
||||
|
|
@ -238,7 +241,7 @@ func testHostScriptResult(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
unsignedScriptResult, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
unsignedScriptResult, _, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: 1,
|
||||
ExecutionID: createdUnsignedScript.ExecutionID,
|
||||
Output: "foo",
|
||||
|
|
@ -261,6 +264,8 @@ func testScripts(t *testing.T, ds *Datastore) {
|
|||
// get unknown script contents
|
||||
_, err = ds.GetScriptContents(ctx, 123)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
_, err = ds.GetAnyScriptContents(ctx, 123)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
|
||||
// create global scriptGlobal
|
||||
scriptGlobal, err := ds.NewScript(ctx, &fleet.Script{
|
||||
|
|
@ -282,6 +287,9 @@ func testScripts(t *testing.T, ds *Datastore) {
|
|||
contents, err := ds.GetScriptContents(ctx, scriptGlobal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo", string(contents))
|
||||
contents, err = ds.GetAnyScriptContents(ctx, scriptGlobal.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo", string(contents))
|
||||
|
||||
// create team script but team does not exist
|
||||
_, err = ds.NewScript(ctx, &fleet.Script{
|
||||
|
|
@ -315,6 +323,9 @@ func testScripts(t *testing.T, ds *Datastore) {
|
|||
contents, err = ds.GetScriptContents(ctx, scriptTeam.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo 'team'", string(contents))
|
||||
contents, err = ds.GetAnyScriptContents(ctx, scriptTeam.ID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "echo 'team'", string(contents))
|
||||
|
||||
// try to create another team script with the same name
|
||||
_, err = ds.NewScript(ctx, &fleet.Script{
|
||||
|
|
@ -781,12 +792,13 @@ func testLockHostViaScript(t *testing.T, ds *Datastore) {
|
|||
require.True(t, status.IsPendingLock())
|
||||
|
||||
// simulate a successful result for the lock script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: s.HostID,
|
||||
ExecutionID: s.ExecutionID,
|
||||
ExitCode: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "lock_ref", action)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: windowsHostID, Platform: "windows", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -832,12 +844,13 @@ func testUnlockHostViaScript(t *testing.T, ds *Datastore) {
|
|||
require.True(t, status.IsPendingUnlock())
|
||||
|
||||
// simulate a successful result for the unlock script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: s.HostID,
|
||||
ExecutionID: s.ExecutionID,
|
||||
ExitCode: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "unlock_ref", action)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: "windows", UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -874,12 +887,13 @@ func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
|||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||
|
||||
// simulate a successful result for the lock script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err := ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.LockScript.ExecutionID,
|
||||
ExitCode: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "lock_ref", action)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -899,12 +913,13 @@ func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
|||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||
|
||||
// simulate a failed result for the unlock script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.UnlockScript.ExecutionID,
|
||||
ExitCode: -1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "unlock_ref", action)
|
||||
|
||||
// still locked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
|
|
@ -925,12 +940,13 @@ func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
|||
checkLockWipeState(t, status, false, true, false, true, false, false)
|
||||
|
||||
// this time simulate a successful result for the unlock script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.UnlockScript.ExecutionID,
|
||||
ExitCode: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "unlock_ref", action)
|
||||
|
||||
// host is now unlocked
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
|
|
@ -951,12 +967,13 @@ func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
|||
checkLockWipeState(t, status, true, false, false, false, true, false)
|
||||
|
||||
// simulate a failed result for the lock script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.LockScript.ExecutionID,
|
||||
ExitCode: 2,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "lock_ref", action)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1009,12 +1026,13 @@ func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
|||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// simulate a failed result for the wipe script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.WipeScript.ExecutionID,
|
||||
ExitCode: 1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "wipe_ref", action)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1034,12 +1052,13 @@ func testLockUnlockWipeViaScripts(t *testing.T, ds *Datastore) {
|
|||
checkLockWipeState(t, status, true, false, false, false, false, true)
|
||||
|
||||
// simulate a successful result for the wipe script execution
|
||||
_, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
_, action, err = ds.SetHostScriptExecutionResult(ctx, &fleet.HostScriptResultPayload{
|
||||
HostID: hostID,
|
||||
ExecutionID: status.WipeScript.ExecutionID,
|
||||
ExitCode: 0,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "wipe_ref", action)
|
||||
|
||||
status, err = ds.GetHostLockWipeStatus(ctx, &fleet.Host{ID: hostID, Platform: platform, UUID: "uuid"})
|
||||
require.NoError(t, err)
|
||||
|
|
@ -1147,6 +1166,7 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
// create a software install that references scripts
|
||||
swi, err := ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install-script",
|
||||
UninstallScript: "uninstall-script",
|
||||
PreInstallQuery: "SELECT 1",
|
||||
PostInstallScript: "post-install-script",
|
||||
InstallerFile: bytes.NewReader([]byte("hello")),
|
||||
|
|
@ -1167,7 +1187,7 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
stmt := `SELECT id, HEX(md5_checksum) as md5_checksum FROM script_contents`
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sc, 4)
|
||||
require.Len(t, sc, 5)
|
||||
|
||||
// this should only remove the script_contents of the saved script, since the sync script is
|
||||
// still "in use" by the script execution
|
||||
|
|
@ -1176,15 +1196,17 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
sc = []scriptContents{}
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sc, 3)
|
||||
require.Len(t, sc, 4)
|
||||
require.ElementsMatch(t, []string{
|
||||
md5ChecksumScriptContent(res.ScriptContents),
|
||||
md5ChecksumScriptContent("install-script"),
|
||||
md5ChecksumScriptContent("post-install-script"),
|
||||
md5ChecksumScriptContent("uninstall-script"),
|
||||
}, []string{
|
||||
sc[0].Checksum,
|
||||
sc[1].Checksum,
|
||||
sc[2].Checksum,
|
||||
sc[3].Checksum,
|
||||
})
|
||||
|
||||
// remove the software installer from the DB
|
||||
|
|
@ -1204,6 +1226,7 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
swi, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
|
||||
PreInstallQuery: "SELECT 1",
|
||||
InstallScript: "install-script",
|
||||
UninstallScript: "uninstall-script",
|
||||
InstallerFile: bytes.NewReader([]byte("hello")),
|
||||
StorageID: "storage1",
|
||||
Filename: "file1",
|
||||
|
|
@ -1219,7 +1242,7 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
sc = []scriptContents{}
|
||||
err = sqlx.SelectContext(ctx, ds.reader(ctx), &sc, stmt)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sc, 2)
|
||||
require.Len(t, sc, 3)
|
||||
|
||||
// remove the software installer from the DB
|
||||
err = ds.DeleteSoftwareInstaller(ctx, swi)
|
||||
|
|
@ -1233,3 +1256,15 @@ func testCleanupUnusedScriptContents(t *testing.T, ds *Datastore) {
|
|||
require.Len(t, sc, 1)
|
||||
require.Equal(t, md5ChecksumScriptContent(res.ScriptContents), sc[0].Checksum)
|
||||
}
|
||||
|
||||
func testGetAnyScriptContents(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
contents := `echo foobar;`
|
||||
res, err := insertScriptContents(ctx, ds.writer(ctx), contents)
|
||||
require.NoError(t, err)
|
||||
id, _ := res.LastInsertId()
|
||||
|
||||
result, err := ds.GetAnyScriptContents(ctx, uint(id))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, contents, string(result))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2155,45 +2155,6 @@ func (ds *Datastore) ListCVEs(ctx context.Context, maxAge time.Duration) ([]flee
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// tblAlias is the table alias to use as prefix for the host_software_installs
|
||||
// column names, no prefix used if empty.
|
||||
// colAlias is the name to be assigned to the computed status column, pass
|
||||
// empty to have the value only, no column alias set.
|
||||
func softwareInstallerHostStatusNamedQuery(tblAlias, colAlias string) string {
|
||||
if tblAlias != "" {
|
||||
tblAlias += "."
|
||||
}
|
||||
if colAlias != "" {
|
||||
colAlias = " AS " + colAlias
|
||||
}
|
||||
// the computed column assumes that all results (pre, install and post) are
|
||||
// stored at once, so that if there is an exit code for the install script
|
||||
// and none for the post-install, it is because there is no post-install.
|
||||
return fmt.Sprintf(`
|
||||
CASE
|
||||
WHEN %[1]sremoved = 1 THEN NULL
|
||||
|
||||
WHEN %[1]spost_install_script_exit_code IS NOT NULL AND
|
||||
%[1]spost_install_script_exit_code = 0 THEN :software_status_installed
|
||||
|
||||
WHEN %[1]spost_install_script_exit_code IS NOT NULL AND
|
||||
%[1]spost_install_script_exit_code != 0 THEN :software_status_failed
|
||||
|
||||
WHEN %[1]sinstall_script_exit_code IS NOT NULL AND
|
||||
%[1]sinstall_script_exit_code = 0 THEN :software_status_installed
|
||||
|
||||
WHEN %[1]sinstall_script_exit_code IS NOT NULL AND
|
||||
%[1]sinstall_script_exit_code != 0 THEN :software_status_failed
|
||||
|
||||
WHEN %[1]spre_install_query_output IS NOT NULL AND
|
||||
%[1]spre_install_query_output = '' THEN :software_status_failed
|
||||
|
||||
WHEN %[1]shost_id IS NOT NULL THEN :software_status_pending
|
||||
|
||||
ELSE NULL -- not installed from Fleet installer
|
||||
END %[2]s `, tblAlias, colAlias)
|
||||
}
|
||||
|
||||
func (ds *Datastore) ListHostSoftware(ctx context.Context, host *fleet.Host, opts fleet.HostSoftwareTitleListOptions) ([]*fleet.HostSoftwareWithInstaller, *fleet.PaginationMetadata, error) {
|
||||
var onlySelfServiceClause string
|
||||
if opts.SelfServiceOnly {
|
||||
|
|
@ -2218,9 +2179,11 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
hs.host_id = :host_id AND
|
||||
s.title_id = st.id
|
||||
) OR `
|
||||
status := fmt.Sprintf(`COALESCE(%s, %s)`, "hsi.last_status", vppAppHostStatusNamedQuery("hvsi", "ncr", ""))
|
||||
if opts.OnlyAvailableForInstall {
|
||||
// Get software that has a package/VPP installer but was not installed with Fleet
|
||||
softwareIsInstalledOnHostClause = ` status IS NULL AND (si.id IS NOT NULL OR vat.adam_id IS NOT NULL) AND ` + softwareIsInstalledOnHostClause
|
||||
softwareIsInstalledOnHostClause = fmt.Sprintf(` %s IS NULL AND (si.id IS NOT NULL OR vat.adam_id IS NOT NULL) AND %s`, status,
|
||||
softwareIsInstalledOnHostClause)
|
||||
}
|
||||
|
||||
// this statement lists only the software that is reported as installed on
|
||||
|
|
@ -2237,16 +2200,64 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
vat.adam_id as vpp_app_adam_id,
|
||||
vap.latest_version as vpp_app_version,
|
||||
NULLIF(vap.icon_url, '') as vpp_app_icon_url,
|
||||
COALESCE(hsi.created_at, hvsi.created_at) as last_install_installed_at,
|
||||
COALESCE(hsi.execution_id, hvsi.command_uuid) as last_install_install_uuid,
|
||||
-- get either the softare installer status or the vpp app status
|
||||
COALESCE(%s, %s) as status
|
||||
COALESCE(hsi.last_installed_at, hvsi.created_at) as last_install_installed_at,
|
||||
COALESCE(hsi.last_install_execution_id, hvsi.command_uuid) as last_install_install_uuid,
|
||||
hsi.last_uninstalled_at as last_uninstall_uninstalled_at,
|
||||
hsi.last_uninstall_execution_id as last_uninstall_script_execution_id,
|
||||
-- get either the software installer status or the vpp app status
|
||||
%s as status
|
||||
FROM
|
||||
software_titles st
|
||||
LEFT OUTER JOIN
|
||||
software_installers si ON st.id = si.title_id AND si.global_or_team_id = :global_or_team_id
|
||||
LEFT OUTER JOIN
|
||||
host_software_installs hsi ON si.id = hsi.software_installer_id AND hsi.host_id = :host_id AND hsi.removed = 0
|
||||
LEFT OUTER JOIN -- get the latest status and install/uninstall attempts (merge 3 host_software_installs rows into 1)
|
||||
(
|
||||
SELECT
|
||||
hsi_group.host_id,
|
||||
hsi_group.software_installer_id,
|
||||
TIMESTAMP(GROUP_CONCAT(hsi_installed_at)) as last_installed_at,
|
||||
GROUP_CONCAT(hsi_install_execution_id) as last_install_execution_id,
|
||||
TIMESTAMP(GROUP_CONCAT(hsi_uninstalled_at)) as last_uninstalled_at,
|
||||
GROUP_CONCAT(hsi_uninstall_execution_id) as last_uninstall_execution_id,
|
||||
IF(GROUP_CONCAT(hsi_status) = '', NULL, GROUP_CONCAT(hsi_status)) as last_status
|
||||
FROM (
|
||||
-- get latest install/uninstall status
|
||||
SELECT
|
||||
host_id, software_installer_id,
|
||||
NULL as hsi_installed_at, NULL as hsi_install_execution_id,
|
||||
NULL as hsi_uninstalled_at, NULL as hsi_uninstall_execution_id,
|
||||
-- get the status of the latest attempt; 27-1 is the length of the timestamp
|
||||
SUBSTRING(MAX(CONCAT(created_at, COALESCE(status, ''))), 27) AS hsi_status
|
||||
FROM host_software_installs
|
||||
WHERE host_id = :host_id AND removed = 0
|
||||
GROUP BY host_id, software_installer_id
|
||||
UNION
|
||||
-- get latest install attempt
|
||||
SELECT
|
||||
host_id, software_installer_id,
|
||||
MAX(created_at) as hsi_installed_at,
|
||||
-- get the execution_id of the latest attempt; 27-1 is the length of the timestamp
|
||||
SUBSTRING(MAX(CONCAT(created_at, execution_id)), 27) AS hsi_install_execution_id,
|
||||
NULL as hsi_uninstalled_at, NULL as hsi_uninstall_execution_id,
|
||||
NULL as hsi_status
|
||||
FROM host_software_installs
|
||||
WHERE host_id = :host_id AND removed = 0 AND uninstall = 0
|
||||
GROUP BY host_id, software_installer_id
|
||||
UNION
|
||||
-- get latest uninstall attempt
|
||||
SELECT
|
||||
host_id, software_installer_id,
|
||||
NULL as hsi_installed_at, NULL as hsi_install_execution_id,
|
||||
MAX(created_at) as hsi_uninstalled_at,
|
||||
-- get the execution_id of the latest attempt; 27-1 is the length of the timestamp
|
||||
SUBSTRING(MAX(CONCAT(created_at, execution_id)), 27) AS hsi_uninstall_execution_id,
|
||||
NULL as hsi_status
|
||||
FROM host_software_installs
|
||||
WHERE host_id = :host_id AND removed = 0 AND uninstall = 1
|
||||
GROUP BY host_id, software_installer_id
|
||||
) as hsi_group
|
||||
GROUP BY hsi_group.host_id, hsi_group.software_installer_id
|
||||
) as hsi ON si.id = hsi.software_installer_id
|
||||
LEFT OUTER JOIN
|
||||
vpp_apps vap ON st.id = vap.title_id AND vap.platform = :host_platform
|
||||
LEFT OUTER JOIN
|
||||
|
|
@ -2256,13 +2267,7 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
LEFT OUTER JOIN
|
||||
nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid
|
||||
WHERE
|
||||
-- use the latest install attempt only
|
||||
( hsi.id IS NULL OR hsi.id = (
|
||||
SELECT hsi2.id
|
||||
FROM host_software_installs hsi2
|
||||
WHERE hsi2.host_id = hsi.host_id AND hsi2.software_installer_id = hsi.software_installer_id AND hsi2.removed = 0
|
||||
ORDER BY hsi2.created_at DESC
|
||||
LIMIT 1 ) ) AND
|
||||
-- use the latest VPP install attempt only
|
||||
( hvsi.id IS NULL OR hvsi.id = (
|
||||
SELECT hvsi2.id
|
||||
FROM host_vpp_software_installs hvsi2
|
||||
|
|
@ -2276,8 +2281,7 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
( %s hsi.host_id IS NOT NULL OR hvsi.host_id IS NOT NULL )
|
||||
%s
|
||||
%s
|
||||
`, softwareInstallerHostStatusNamedQuery("hsi", ""), vppAppHostStatusNamedQuery("hvsi", "ncr", ""),
|
||||
softwareIsInstalledOnHostClause, onlySelfServiceClause, onlyVulnerableClause)
|
||||
`, status, softwareIsInstalledOnHostClause, onlySelfServiceClause, onlyVulnerableClause)
|
||||
|
||||
// this statement lists only the software that has never been installed nor
|
||||
// attempted to be installed on the host, but that is available to be
|
||||
|
|
@ -2296,6 +2300,8 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
NULLIF(vap.icon_url, '') as vpp_app_icon_url,
|
||||
NULL as last_install_installed_at,
|
||||
NULL as last_install_install_uuid,
|
||||
NULL as last_uninstall_uninstalled_at,
|
||||
NULL as last_uninstall_script_execution_id,
|
||||
NULL as status
|
||||
FROM
|
||||
software_titles st
|
||||
|
|
@ -2359,6 +2365,8 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
vpp_app_icon_url,
|
||||
last_install_installed_at,
|
||||
last_install_install_uuid,
|
||||
last_uninstall_uninstalled_at,
|
||||
last_uninstall_script_execution_id,
|
||||
status
|
||||
`
|
||||
|
||||
|
|
@ -2369,9 +2377,9 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
namedArgs := map[string]any{
|
||||
"host_id": host.ID,
|
||||
"host_platform": host.FleetPlatform(),
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"software_status_failed": fleet.SoftwareInstallFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallPending,
|
||||
"software_status_installed": fleet.SoftwareInstalled,
|
||||
"mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged,
|
||||
"mdm_status_error": fleet.MDMAppleStatusError,
|
||||
"mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError,
|
||||
|
|
@ -2419,15 +2427,17 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
|
||||
type hostSoftware struct {
|
||||
fleet.HostSoftwareWithInstaller
|
||||
LastInstallInstalledAt *time.Time `db:"last_install_installed_at"`
|
||||
LastInstallInstallUUID *string `db:"last_install_install_uuid"`
|
||||
PackageSelfService *bool `db:"package_self_service"`
|
||||
PackageName *string `db:"package_name"`
|
||||
PackageVersion *string `db:"package_version"`
|
||||
VPPAppSelfService *bool `db:"vpp_app_self_service"`
|
||||
VPPAppAdamID *string `db:"vpp_app_adam_id"`
|
||||
VPPAppVersion *string `db:"vpp_app_version"`
|
||||
VPPAppIconURL *string `db:"vpp_app_icon_url"`
|
||||
LastInstallInstalledAt *time.Time `db:"last_install_installed_at"`
|
||||
LastInstallInstallUUID *string `db:"last_install_install_uuid"`
|
||||
LastUninstallUninstalledAt *time.Time `db:"last_uninstall_uninstalled_at"`
|
||||
LastUninstallScriptExecutionID *string `db:"last_uninstall_script_execution_id"`
|
||||
PackageSelfService *bool `db:"package_self_service"`
|
||||
PackageName *string `db:"package_name"`
|
||||
PackageVersion *string `db:"package_version"`
|
||||
VPPAppSelfService *bool `db:"vpp_app_self_service"`
|
||||
VPPAppAdamID *string `db:"vpp_app_adam_id"`
|
||||
VPPAppVersion *string `db:"vpp_app_version"`
|
||||
VPPAppIconURL *string `db:"vpp_app_icon_url"`
|
||||
}
|
||||
var hostSoftwareList []*hostSoftware
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostSoftwareList, stmt, args...); err != nil {
|
||||
|
|
@ -2461,6 +2471,16 @@ AND EXISTS (SELECT 1 FROM software s JOIN software_cve scve ON scve.software_id
|
|||
hs.SoftwarePackage.LastInstall.InstalledAt = *hs.LastInstallInstalledAt
|
||||
}
|
||||
}
|
||||
|
||||
// promote the last uninstall info to the proper destination fields
|
||||
if hs.LastUninstallScriptExecutionID != nil && *hs.LastUninstallScriptExecutionID != "" {
|
||||
hs.SoftwarePackage.LastUninstall = &fleet.HostSoftwareUninstall{
|
||||
ExecutionID: *hs.LastUninstallScriptExecutionID,
|
||||
}
|
||||
if hs.LastUninstallUninstalledAt != nil {
|
||||
hs.SoftwarePackage.LastUninstall.UninstalledAt = *hs.LastUninstallUninstalledAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// promote the VPP app id and version to the proper destination fields
|
||||
|
|
@ -2668,11 +2688,8 @@ SELECT
|
|||
0 as vpp_apps_count
|
||||
FROM software_titles st
|
||||
INNER JOIN software_installers si ON si.title_id = st.id
|
||||
INNER JOIN host_software_installs hsi ON hsi.host_id = ? AND hsi.software_installer_id = si.id
|
||||
WHERE hsi.removed = 0 AND
|
||||
-- :software_status_installed
|
||||
((hsi.post_install_script_exit_code IS NOT NULL AND hsi.post_install_script_exit_code = 0) OR
|
||||
(hsi.install_script_exit_code IS NOT NULL AND hsi.install_script_exit_code = 0))
|
||||
INNER JOIN host_software_installs hsi ON hsi.host_id = :host_id AND hsi.software_installer_id = si.id
|
||||
WHERE hsi.removed = 0 AND hsi.status = :software_status_installed
|
||||
|
||||
UNION
|
||||
|
||||
|
|
@ -2685,12 +2702,21 @@ SELECT
|
|||
1 as vpp_apps_count
|
||||
FROM software_titles st
|
||||
INNER JOIN vpp_apps vap ON vap.title_id = st.id
|
||||
INNER JOIN host_vpp_software_installs hvsi ON hvsi.host_id = ? AND hvsi.adam_id = vap.adam_id AND hvsi.platform = vap.platform
|
||||
INNER JOIN host_vpp_software_installs hvsi ON hvsi.host_id = :host_id AND hvsi.adam_id = vap.adam_id AND hvsi.platform = vap.platform
|
||||
INNER JOIN nano_command_results ncr ON ncr.command_uuid = hvsi.command_uuid
|
||||
WHERE hvsi.removed = 0 AND ncr.status = ?
|
||||
WHERE hvsi.removed = 0 AND ncr.status = :mdm_status_acknowledged
|
||||
`
|
||||
selectStmt, args, err := sqlx.Named(stmt, map[string]interface{}{
|
||||
"host_id": hostID,
|
||||
"software_status_installed": fleet.SoftwareInstalled,
|
||||
"mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build query to get installed software titles")
|
||||
}
|
||||
|
||||
var titles []fleet.SoftwareTitle
|
||||
if err := sqlx.SelectContext(ctx, qc, &titles, stmt, hostID, hostID, fleet.MDMAppleStatusAcknowledged); err != nil {
|
||||
if err := sqlx.SelectContext(ctx, qc, &titles, selectStmt, args...); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get installed software titles")
|
||||
}
|
||||
return titles, nil
|
||||
|
|
|
|||
|
|
@ -24,14 +24,12 @@ func (ds *Datastore) ListPendingSoftwareInstalls(ctx context.Context, hostID uin
|
|||
WHERE
|
||||
host_id = ?
|
||||
AND
|
||||
install_script_exit_code IS NULL
|
||||
AND
|
||||
pre_install_query_output IS NULL
|
||||
status = ?
|
||||
ORDER BY
|
||||
created_at ASC
|
||||
`
|
||||
var results []string
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, hostID); err != nil {
|
||||
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &results, stmt, hostID, fleet.SoftwareInstallPending); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "list pending software installs")
|
||||
}
|
||||
return results, nil
|
||||
|
|
@ -86,6 +84,11 @@ func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload
|
|||
return 0, ctxerr.Wrap(ctx, err, "get or generate install script contents ID")
|
||||
}
|
||||
|
||||
uninstallScriptID, err := ds.getOrGenerateScriptContentsID(ctx, payload.UninstallScript)
|
||||
if err != nil {
|
||||
return 0, ctxerr.Wrap(ctx, err, "get or generate uninstall script contents ID")
|
||||
}
|
||||
|
||||
var postInstallScriptID *uint
|
||||
if payload.PostInstallScript != "" {
|
||||
sid, err := ds.getOrGenerateScriptContentsID(ctx, payload.PostInstallScript)
|
||||
|
|
@ -113,15 +116,17 @@ INSERT INTO software_installers (
|
|||
storage_id,
|
||||
filename,
|
||||
version,
|
||||
package_ids,
|
||||
install_script_content_id,
|
||||
pre_install_query,
|
||||
post_install_script_content_id,
|
||||
uninstall_script_content_id,
|
||||
platform,
|
||||
self_service,
|
||||
user_id,
|
||||
user_name,
|
||||
user_email
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?))`
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?))`
|
||||
|
||||
args := []interface{}{
|
||||
tid,
|
||||
|
|
@ -130,9 +135,11 @@ INSERT INTO software_installers (
|
|||
payload.StorageID,
|
||||
payload.Filename,
|
||||
payload.Version,
|
||||
strings.Join(payload.PackageIDs, ","),
|
||||
installScriptID,
|
||||
payload.PreInstallQuery,
|
||||
postInstallScriptID,
|
||||
uninstallScriptID,
|
||||
payload.Platform,
|
||||
payload.SelfService,
|
||||
payload.UserID,
|
||||
|
|
@ -215,6 +222,7 @@ SELECT
|
|||
si.install_script_content_id,
|
||||
si.pre_install_query,
|
||||
si.post_install_script_content_id,
|
||||
si.uninstall_script_content_id,
|
||||
si.uploaded_at,
|
||||
COALESCE(st.name, '') AS software_title,
|
||||
si.platform
|
||||
|
|
@ -239,9 +247,10 @@ WHERE
|
|||
func (ds *Datastore) GetSoftwareInstallerMetadataByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint, withScriptContents bool) (*fleet.SoftwareInstaller, error) {
|
||||
var scriptContentsSelect, scriptContentsFrom string
|
||||
if withScriptContents {
|
||||
scriptContentsSelect = ` , inst.contents AS install_script, COALESCE(pisnt.contents, '') AS post_install_script `
|
||||
scriptContentsSelect = ` , inst.contents AS install_script, COALESCE(pinst.contents, '') AS post_install_script, uninst.contents AS uninstall_script `
|
||||
scriptContentsFrom = ` LEFT OUTER JOIN script_contents inst ON inst.id = si.install_script_content_id
|
||||
LEFT OUTER JOIN script_contents pisnt ON pisnt.id = si.post_install_script_content_id `
|
||||
LEFT OUTER JOIN script_contents pinst ON pinst.id = si.post_install_script_content_id
|
||||
LEFT OUTER JOIN script_contents uninst ON uninst.id = si.uninstall_script_content_id`
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
|
|
@ -255,6 +264,7 @@ SELECT
|
|||
si.install_script_content_id,
|
||||
si.pre_install_query,
|
||||
si.post_install_script_content_id,
|
||||
si.uninstall_script_content_id,
|
||||
si.uploaded_at,
|
||||
si.self_service,
|
||||
COALESCE(st.name, '') AS software_title
|
||||
|
|
@ -349,6 +359,41 @@ func (ds *Datastore) InsertSoftwareInstallRequest(ctx context.Context, hostID ui
|
|||
return installID, ctxerr.Wrap(ctx, err, "inserting new install software request")
|
||||
}
|
||||
|
||||
func (ds *Datastore) InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
|
||||
const (
|
||||
insertStmt = `
|
||||
INSERT INTO host_software_installs
|
||||
(execution_id, host_id, software_installer_id, user_id, uninstall)
|
||||
VALUES
|
||||
(?, ?, ?, ?, 1)
|
||||
`
|
||||
hostExistsStmt = `SELECT 1 FROM hosts WHERE id = ?`
|
||||
)
|
||||
|
||||
// we need to explicitly do this check here because we can't set a FK constraint on the schema
|
||||
var hostExists bool
|
||||
err := sqlx.GetContext(ctx, ds.reader(ctx), &hostExists, hostExistsStmt, hostID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return notFound("Host").WithID(hostID)
|
||||
}
|
||||
return ctxerr.Wrap(ctx, err, "checking if host exists")
|
||||
}
|
||||
|
||||
var userID *uint
|
||||
if ctxUser := authz.UserFromContext(ctx); ctxUser != nil {
|
||||
userID = &ctxUser.ID
|
||||
}
|
||||
_, err = ds.writer(ctx).ExecContext(ctx, insertStmt,
|
||||
executionID,
|
||||
hostID,
|
||||
softwareInstallerID,
|
||||
userID,
|
||||
)
|
||||
|
||||
return ctxerr.Wrap(ctx, err, "inserting new uninstall software request")
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetSoftwareInstallResults(ctx context.Context, resultsUUID string) (*fleet.HostSoftwareInstallerResult, error) {
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
|
|
@ -359,7 +404,7 @@ SELECT
|
|||
hsi.host_id AS host_id,
|
||||
st.name AS software_title,
|
||||
st.id AS software_title_id,
|
||||
COALESCE(%s, '') AS status,
|
||||
COALESCE(hsi.status, '') AS status,
|
||||
si.filename AS software_package,
|
||||
hsi.user_id AS user_id,
|
||||
hsi.post_install_script_exit_code,
|
||||
|
|
@ -375,13 +420,10 @@ FROM
|
|||
JOIN software_titles st ON si.title_id = st.id
|
||||
WHERE
|
||||
hsi.execution_id = :execution_id
|
||||
`, softwareInstallerHostStatusNamedQuery("hsi", ""))
|
||||
`)
|
||||
|
||||
stmt, args, err := sqlx.Named(query, map[string]any{
|
||||
"execution_id": resultsUUID,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"execution_id": resultsUUID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build named query for get software install results")
|
||||
|
|
@ -404,13 +446,15 @@ func (ds *Datastore) GetSummaryHostSoftwareInstalls(ctx context.Context, install
|
|||
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
COALESCE(SUM( IF(status = :software_status_pending, 1, 0)), 0) AS pending,
|
||||
COALESCE(SUM( IF(status = :software_status_failed, 1, 0)), 0) AS failed,
|
||||
COALESCE(SUM( IF(status = :software_status_pending_install, 1, 0)), 0) AS pending_install,
|
||||
COALESCE(SUM( IF(status = :software_status_failed_install, 1, 0)), 0) AS failed_install,
|
||||
COALESCE(SUM( IF(status = :software_status_pending_uninstall, 1, 0)), 0) AS pending_uninstall,
|
||||
COALESCE(SUM( IF(status = :software_status_failed_uninstall, 1, 0)), 0) AS failed_uninstall,
|
||||
COALESCE(SUM( IF(status = :software_status_installed, 1, 0)), 0) AS installed
|
||||
FROM (
|
||||
SELECT
|
||||
software_installer_id,
|
||||
%s
|
||||
status
|
||||
FROM
|
||||
host_software_installs hsi
|
||||
WHERE
|
||||
|
|
@ -424,13 +468,15 @@ WHERE
|
|||
AND host_deleted_at IS NULL
|
||||
AND removed = 0
|
||||
GROUP BY
|
||||
host_id)) s`, softwareInstallerHostStatusNamedQuery("hsi", "status"))
|
||||
host_id)) s`)
|
||||
|
||||
query, args, err := sqlx.Named(stmt, map[string]interface{}{
|
||||
"installer_id": installerID,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"installer_id": installerID,
|
||||
"software_status_pending_install": fleet.SoftwareInstallPending,
|
||||
"software_status_failed_install": fleet.SoftwareInstallFailed,
|
||||
"software_status_pending_uninstall": fleet.SoftwareUninstallPending,
|
||||
"software_status_failed_uninstall": fleet.SoftwareUninstallFailed,
|
||||
"software_status_installed": fleet.SoftwareInstalled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get summary host software installs: named query")
|
||||
|
|
@ -445,6 +491,15 @@ WHERE
|
|||
}
|
||||
|
||||
func (ds *Datastore) vppAppJoin(appID fleet.VPPAppID, status fleet.SoftwareInstallerStatus) (string, []interface{}, error) {
|
||||
// Since VPP does not have uninstaller yet, we map the generic pending/failed statuses to the install statuses
|
||||
switch status {
|
||||
case fleet.SoftwarePending:
|
||||
status = fleet.SoftwareInstallPending
|
||||
case fleet.SoftwareFailed:
|
||||
status = fleet.SoftwareInstallFailed
|
||||
default:
|
||||
// no change
|
||||
}
|
||||
stmt := fmt.Sprintf(`JOIN (
|
||||
SELECT
|
||||
host_id
|
||||
|
|
@ -469,9 +524,9 @@ WHERE
|
|||
"status": status,
|
||||
"adam_id": appID.AdamID,
|
||||
"platform": appID.Platform,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"software_status_installed": fleet.SoftwareInstalled,
|
||||
"software_status_failed": fleet.SoftwareInstallFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallPending,
|
||||
"mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged,
|
||||
"mdm_status_error": fleet.MDMAppleStatusError,
|
||||
"mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError,
|
||||
|
|
@ -479,6 +534,21 @@ WHERE
|
|||
}
|
||||
|
||||
func (ds *Datastore) softwareInstallerJoin(installerID uint, status fleet.SoftwareInstallerStatus) (string, []interface{}, error) {
|
||||
statusFilter := "hsi.status = :status"
|
||||
var status2 fleet.SoftwareInstallerStatus
|
||||
switch status {
|
||||
case fleet.SoftwarePending:
|
||||
status = fleet.SoftwareInstallPending
|
||||
status2 = fleet.SoftwareUninstallPending
|
||||
case fleet.SoftwareFailed:
|
||||
status = fleet.SoftwareInstallFailed
|
||||
status2 = fleet.SoftwareUninstallFailed
|
||||
default:
|
||||
// no change
|
||||
}
|
||||
if status2 != "" {
|
||||
statusFilter = "hsi.status IN (:status, :status2)"
|
||||
}
|
||||
stmt := fmt.Sprintf(`JOIN (
|
||||
SELECT
|
||||
host_id
|
||||
|
|
@ -495,21 +565,19 @@ WHERE
|
|||
AND removed = 0
|
||||
GROUP BY
|
||||
host_id, software_installer_id)
|
||||
AND (%s) = :status) hss ON hss.host_id = h.id
|
||||
`, softwareInstallerHostStatusNamedQuery("hsi", ""))
|
||||
AND %s) hss ON hss.host_id = h.id
|
||||
`, statusFilter)
|
||||
|
||||
return sqlx.Named(stmt, map[string]interface{}{
|
||||
"status": status,
|
||||
"installer_id": installerID,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"status": status,
|
||||
"status2": status2,
|
||||
"installer_id": installerID,
|
||||
})
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetHostLastInstallData(ctx context.Context, hostID, installerID uint) (*fleet.HostLastInstallData, error) {
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT execution_id, %s AS status
|
||||
SELECT execution_id, hsi.status
|
||||
FROM host_software_installs hsi
|
||||
WHERE hsi.id = (
|
||||
SELECT
|
||||
|
|
@ -519,14 +587,11 @@ func (ds *Datastore) GetHostLastInstallData(ctx context.Context, hostID, install
|
|||
software_installer_id = :installer_id AND host_id = :host_id
|
||||
GROUP BY
|
||||
host_id, software_installer_id)
|
||||
`, softwareInstallerHostStatusNamedQuery("hsi", ""))
|
||||
`)
|
||||
|
||||
stmt, args, err := sqlx.Named(stmt, map[string]interface{}{
|
||||
"host_id": hostID,
|
||||
"installer_id": installerID,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"host_id": hostID,
|
||||
"installer_id": installerID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build named query to get host last install data")
|
||||
|
|
@ -623,6 +688,7 @@ INSERT INTO software_installers (
|
|||
filename,
|
||||
version,
|
||||
install_script_content_id,
|
||||
uninstall_script_content_id,
|
||||
pre_install_query,
|
||||
post_install_script_content_id,
|
||||
platform,
|
||||
|
|
@ -633,12 +699,13 @@ INSERT INTO software_installers (
|
|||
user_email,
|
||||
url
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?,
|
||||
(SELECT id FROM software_titles WHERE name = ? AND source = ? AND browser = ''),
|
||||
?, (SELECT name FROM users WHERE id = ?), (SELECT email FROM users WHERE id = ?), ?
|
||||
)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
install_script_content_id = VALUES(install_script_content_id),
|
||||
uninstall_script_content_id = VALUES(uninstall_script_content_id),
|
||||
post_install_script_content_id = VALUES(post_install_script_content_id),
|
||||
storage_id = VALUES(storage_id),
|
||||
filename = VALUES(filename),
|
||||
|
|
@ -732,6 +799,12 @@ WHERE global_or_team_id = ?
|
|||
}
|
||||
installScriptID, _ := isRes.LastInsertId()
|
||||
|
||||
uisRes, err := insertScriptContents(ctx, tx, installer.UninstallScript)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "inserting uninstall script contents for software installer with name %q", installer.Filename)
|
||||
}
|
||||
uninstallScriptID, _ := uisRes.LastInsertId()
|
||||
|
||||
var postInstallScriptID *int64
|
||||
if installer.PostInstallScript != "" {
|
||||
pisRes, err := insertScriptContents(ctx, tx, installer.PostInstallScript)
|
||||
|
|
@ -750,6 +823,7 @@ WHERE global_or_team_id = ?
|
|||
installer.Filename,
|
||||
installer.Version,
|
||||
installScriptID,
|
||||
uninstallScriptID,
|
||||
installer.PreInstallQuery,
|
||||
postInstallScriptID,
|
||||
installer.Platform,
|
||||
|
|
@ -805,3 +879,19 @@ func (ds *Datastore) HasSelfServiceSoftwareInstallers(ctx context.Context, hostP
|
|||
}
|
||||
return hasInstallers, nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetSoftwareTitleNameFromExecutionID(ctx context.Context, executionID string) (string, error) {
|
||||
stmt := `
|
||||
SELECT name
|
||||
FROM software_titles st
|
||||
INNER JOIN software_installers si ON si.title_id = st.id
|
||||
INNER JOIN host_software_installs hsi ON hsi.software_installer_id = si.id
|
||||
WHERE hsi.execution_id = ?
|
||||
`
|
||||
var name string
|
||||
err := sqlx.GetContext(ctx, ds.reader(ctx), &name, stmt, executionID)
|
||||
if err != nil {
|
||||
return "", ctxerr.Wrap(ctx, err, "get software title name from execution ID")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
|
@ -118,7 +119,7 @@ func testListPendingSoftwareInstalls(t *testing.T, ds *Datastore) {
|
|||
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
HostID: host2.ID,
|
||||
InstallUUID: hostInstall5,
|
||||
PreInstallConditionOutput: ptr.String("output"),
|
||||
PreInstallConditionOutput: ptr.String(""), // pre-install query did not return results, so install failed
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -211,24 +212,128 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
|
|||
_, err = ds.InsertSoftwareInstallRequest(ctx, 12, si.InstallerID, false)
|
||||
require.ErrorAs(t, err, &nfe)
|
||||
|
||||
// successful insert
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tc),
|
||||
// Host with software install pending
|
||||
tag := "-pending_install"
|
||||
hostPendingInstall, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tag + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tag + tc),
|
||||
UUID: uuid.NewString(),
|
||||
Platform: "darwin",
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertSoftwareInstallRequest(ctx, host.ID, si.InstallerID, false)
|
||||
_, err = ds.InsertSoftwareInstallRequest(ctx, hostPendingInstall.ID, si.InstallerID, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// list hosts with software install requests
|
||||
// Host with software install failed
|
||||
tag = "-failed_install"
|
||||
hostFailedInstall, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tag + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tag + tc),
|
||||
UUID: uuid.NewString(),
|
||||
Platform: "darwin",
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertSoftwareInstallRequest(ctx, hostFailedInstall.ID, si.InstallerID, false)
|
||||
require.NoError(t, err)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err = q.ExecContext(ctx, `
|
||||
UPDATE host_software_installs SET install_script_exit_code = 1 WHERE host_id = ? AND software_installer_id = ?`,
|
||||
hostFailedInstall.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Host with software install successful
|
||||
tag = "-installed"
|
||||
hostInstalled, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tag + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tag + tc),
|
||||
UUID: uuid.NewString(),
|
||||
Platform: "darwin",
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = ds.InsertSoftwareInstallRequest(ctx, hostInstalled.ID, si.InstallerID, false)
|
||||
require.NoError(t, err)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err = q.ExecContext(ctx, `
|
||||
UPDATE host_software_installs SET install_script_exit_code = 0 WHERE host_id = ? AND software_installer_id = ?`,
|
||||
hostInstalled.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Host with pending uninstall
|
||||
tag = "-pending_uninstall"
|
||||
hostPendingUninstall, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tag + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tag + tc),
|
||||
UUID: uuid.NewString(),
|
||||
Platform: "darwin",
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostPendingUninstall.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Host with failed uninstall
|
||||
tag = "-failed_uninstall"
|
||||
hostFailedUninstall, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tag + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tag + tc),
|
||||
UUID: uuid.NewString(),
|
||||
Platform: "darwin",
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostFailedUninstall.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err = q.ExecContext(ctx, `
|
||||
UPDATE host_software_installs SET uninstall_script_exit_code = 1 WHERE host_id = ? AND software_installer_id = ?`,
|
||||
hostFailedUninstall.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Host with successful uninstall
|
||||
tag = "-uninstalled"
|
||||
hostUninstalled, err := ds.NewHost(ctx, &fleet.Host{
|
||||
Hostname: "macos-test" + tag + tc,
|
||||
OsqueryHostID: ptr.String("osquery-macos" + tag + tc),
|
||||
NodeKey: ptr.String("node-key-macos" + tag + tc),
|
||||
UUID: uuid.NewString(),
|
||||
Platform: "darwin",
|
||||
TeamID: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, hostUninstalled.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
_, err = q.ExecContext(ctx, `
|
||||
UPDATE host_software_installs SET uninstall_script_exit_code = 0 WHERE host_id = ? AND software_installer_id = ?`,
|
||||
hostUninstalled.ID, si.InstallerID)
|
||||
require.NoError(t, err)
|
||||
return nil
|
||||
})
|
||||
|
||||
// Uninstall request with unknown host
|
||||
err = ds.InsertSoftwareUninstallRequest(ctx, "uuid"+tag+tc, 99999, si.InstallerID)
|
||||
assert.ErrorContains(t, err, "Host")
|
||||
|
||||
userTeamFilter := fleet.TeamFilter{
|
||||
User: &fleet.User{GlobalRole: ptr.String("admin")},
|
||||
}
|
||||
expectStatus := fleet.SoftwareInstallerPending
|
||||
|
||||
// list hosts with software install pending requests
|
||||
expectStatus := fleet.SoftwareInstallPending
|
||||
hosts, err := ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
|
|
@ -237,15 +342,98 @@ func testSoftwareInstallRequests(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 1)
|
||||
require.Equal(t, host.ID, hosts[0].ID)
|
||||
require.Equal(t, hostPendingInstall.ID, hosts[0].ID)
|
||||
|
||||
// list hosts with all pending requests
|
||||
expectStatus = fleet.SoftwarePending
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
SoftwareStatusFilter: &expectStatus,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 2)
|
||||
assert.ElementsMatch(t, []uint{hostPendingInstall.ID, hostPendingUninstall.ID}, []uint{hosts[0].ID, hosts[1].ID})
|
||||
|
||||
// list hosts with software install failed requests
|
||||
expectStatus = fleet.SoftwareInstallFailed
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
SoftwareStatusFilter: &expectStatus,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 1)
|
||||
assert.ElementsMatch(t, []uint{hostFailedInstall.ID}, []uint{hosts[0].ID})
|
||||
|
||||
// list hosts with all failed requests
|
||||
expectStatus = fleet.SoftwareFailed
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
SoftwareStatusFilter: &expectStatus,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 2)
|
||||
assert.ElementsMatch(t, []uint{hostFailedInstall.ID, hostFailedUninstall.ID}, []uint{hosts[0].ID, hosts[1].ID})
|
||||
|
||||
// list hosts with software installed
|
||||
expectStatus = fleet.SoftwareInstalled
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
SoftwareStatusFilter: &expectStatus,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 1)
|
||||
assert.ElementsMatch(t, []uint{hostInstalled.ID}, []uint{hosts[0].ID})
|
||||
|
||||
// list hosts with pending software uninstall requests
|
||||
expectStatus = fleet.SoftwareUninstallPending
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
SoftwareStatusFilter: &expectStatus,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 1)
|
||||
assert.ElementsMatch(t, []uint{hostPendingUninstall.ID}, []uint{hosts[0].ID})
|
||||
|
||||
// list hosts with failed software uninstall requests
|
||||
expectStatus = fleet.SoftwareUninstallFailed
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
SoftwareStatusFilter: &expectStatus,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, hosts, 1)
|
||||
assert.ElementsMatch(t, []uint{hostFailedUninstall.ID}, []uint{hosts[0].ID})
|
||||
|
||||
// list all hosts with the software title that shows up in host_software (after fleetd software query is run)
|
||||
hosts, err = ds.ListHosts(ctx, userTeamFilter, fleet.HostListOptions{
|
||||
ListOptions: fleet.ListOptions{PerPage: 100},
|
||||
SoftwareTitleIDFilter: installerMeta.TitleID,
|
||||
TeamFilter: teamID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, hosts)
|
||||
|
||||
// get software title includes status
|
||||
summary, err := ds.GetSummaryHostSoftwareInstalls(ctx, installerMeta.InstallerID)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fleet.SoftwareInstallerStatusSummary{
|
||||
Installed: 0,
|
||||
Pending: 1,
|
||||
Failed: 0,
|
||||
Installed: 1,
|
||||
PendingInstall: 1,
|
||||
FailedInstall: 1,
|
||||
PendingUninstall: 1,
|
||||
FailedUninstall: 1,
|
||||
}, *summary)
|
||||
})
|
||||
}
|
||||
|
|
@ -271,27 +459,27 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
}{
|
||||
{
|
||||
name: "pending install",
|
||||
expectedStatus: fleet.SoftwareInstallerPending,
|
||||
expectedStatus: fleet.SoftwareInstallPending,
|
||||
postInstallScriptOutput: ptr.String("post install output"),
|
||||
installScriptOutput: ptr.String("install output"),
|
||||
},
|
||||
{
|
||||
name: "failing install post install script",
|
||||
expectedStatus: fleet.SoftwareInstallerFailed,
|
||||
expectedStatus: fleet.SoftwareInstallFailed,
|
||||
postInstallScriptEC: ptr.Int(1),
|
||||
postInstallScriptOutput: ptr.String("post install output"),
|
||||
installScriptOutput: ptr.String("install output"),
|
||||
},
|
||||
{
|
||||
name: "failing install install script",
|
||||
expectedStatus: fleet.SoftwareInstallerFailed,
|
||||
expectedStatus: fleet.SoftwareInstallFailed,
|
||||
installScriptEC: ptr.Int(1),
|
||||
postInstallScriptOutput: ptr.String("post install output"),
|
||||
installScriptOutput: ptr.String("install output"),
|
||||
},
|
||||
{
|
||||
name: "failing install pre install query",
|
||||
expectedStatus: fleet.SoftwareInstallerFailed,
|
||||
expectedStatus: fleet.SoftwareInstallFailed,
|
||||
preInstallQueryOutput: ptr.String(""),
|
||||
postInstallScriptOutput: ptr.String("post install output"),
|
||||
installScriptOutput: ptr.String("install output"),
|
||||
|
|
@ -812,7 +1000,7 @@ func testGetHostLastInstallData(t *testing.T, ds *Datastore) {
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status)
|
||||
|
||||
// Set result of last installation.
|
||||
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
|
|
@ -829,7 +1017,7 @@ func testGetHostLastInstallData(t *testing.T, ds *Datastore) {
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerInstalled, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstalled, *host1LastInstall.Status)
|
||||
|
||||
// Install installer2.pkg on host1.
|
||||
installUUID2, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID2, false)
|
||||
|
|
@ -842,14 +1030,14 @@ func testGetHostLastInstallData(t *testing.T, ds *Datastore) {
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID1, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerInstalled, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstalled, *host1LastInstall.Status)
|
||||
// Last installation for installer2.pkg should be "pending".
|
||||
host1LastInstall, err = ds.GetHostLastInstallData(ctx, host1.ID, softwareInstallerID2)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID2, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status)
|
||||
|
||||
// Perform another installation of installer1.pkg.
|
||||
installUUID3, err := ds.InsertSoftwareInstallRequest(ctx, host1.ID, softwareInstallerID1, false)
|
||||
|
|
@ -862,7 +1050,7 @@ func testGetHostLastInstallData(t *testing.T, ds *Datastore) {
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID3, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status)
|
||||
|
||||
// Set result of last installer1.pkg installation.
|
||||
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
|
|
@ -879,7 +1067,7 @@ func testGetHostLastInstallData(t *testing.T, ds *Datastore) {
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, installUUID3, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerFailed, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallFailed, *host1LastInstall.Status)
|
||||
|
||||
// No installations on host2.
|
||||
host2LastInstall, err := ds.GetHostLastInstallData(ctx, host2.ID, softwareInstallerID1)
|
||||
|
|
|
|||
|
|
@ -3323,11 +3323,19 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
compareResults := func(expected map[string]fleet.HostSoftwareWithInstaller, got []*fleet.HostSoftwareWithInstaller, expectAsc bool, expectOmitted ...string) {
|
||||
require.Len(t, got, len(expected)-len(expectOmitted))
|
||||
gotToString := func() string {
|
||||
var builder strings.Builder
|
||||
builder.WriteString("Got:\n")
|
||||
for _, g := range got {
|
||||
builder.WriteString(fmt.Sprintf("%+v\n", g))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
require.Len(t, got, len(expected)-len(expectOmitted), gotToString())
|
||||
prev := ""
|
||||
for _, g := range got {
|
||||
e, ok := expected[g.Name+g.Source]
|
||||
require.True(t, ok)
|
||||
require.True(t, ok, "unexpected software %s%s", g.Name, g.Source)
|
||||
require.Equal(t, e.Name, g.Name)
|
||||
require.Equal(t, e.Source, g.Source)
|
||||
if e.SoftwarePackage != nil {
|
||||
|
|
@ -3341,6 +3349,10 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, e.SoftwarePackage.LastInstall.InstallUUID, g.SoftwarePackage.LastInstall.InstallUUID)
|
||||
require.NotNil(t, g.SoftwarePackage.LastInstall.InstalledAt)
|
||||
}
|
||||
if e.SoftwarePackage.LastUninstall != nil {
|
||||
assert.Equal(t, e.SoftwarePackage.LastUninstall.ExecutionID, g.SoftwarePackage.LastUninstall.ExecutionID)
|
||||
assert.NotNil(t, g.SoftwarePackage.LastUninstall.UninstalledAt)
|
||||
}
|
||||
}
|
||||
|
||||
if e.AppStoreApp != nil {
|
||||
|
|
@ -3410,7 +3422,8 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
tm, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
var swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm uint
|
||||
const numberOfSoftwareInstallers = 8
|
||||
var swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm, swi6PendingUninstall, swi7FailedUninstall, swi8Uninstalled uint
|
||||
var otherHostI1UUID, otherHostI2UUID string
|
||||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
|
||||
// keep title id of software B, will use it to associate an installer with it
|
||||
|
|
@ -3428,10 +3441,19 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
scriptContentID, _ := res.LastInsertId()
|
||||
|
||||
// create the uninstall script content (same for all installers, doesn't matter)
|
||||
uninstallScript := `echo 'bar'`
|
||||
resUninstall, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`,
|
||||
uninstallScript, uninstallScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uninstallScriptContentID, _ := resUninstall.LastInsertId()
|
||||
|
||||
// create software titles for all but swi1Pending (will be linked to
|
||||
// existing software title b)
|
||||
var titleIDs []uint
|
||||
for i := 0; i < 4; i++ {
|
||||
for i := 0; i < numberOfSoftwareInstallers-1; i++ {
|
||||
res, err := q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES (?, 'apps')`, fmt.Sprintf("i%d", i))
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -3441,7 +3463,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
var swiIDs []uint
|
||||
for i := 0; i < 5; i++ {
|
||||
for i := 0; i < numberOfSoftwareInstallers; i++ {
|
||||
var (
|
||||
titleID uint
|
||||
teamID *uint
|
||||
|
|
@ -3458,10 +3480,12 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
res, err := q.ExecContext(ctx, `
|
||||
INSERT INTO software_installers
|
||||
(team_id, global_or_team_id, title_id, filename, version, install_script_content_id, storage_id, platform, self_service)
|
||||
(team_id, global_or_team_id, title_id, filename, version, install_script_content_id, uninstall_script_content_id, storage_id, platform, self_service)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, unhex(?), ?, ?)`,
|
||||
teamID, globalOrTeamID, titleID, fmt.Sprintf("installer-%d.pkg", i), fmt.Sprintf("v%d.0.0", i), scriptContentID, hex.EncodeToString([]byte("test")), "darwin", i < 2)
|
||||
(?, ?, ?, ?, ?, ?, ?, unhex(?), ?, ?)`,
|
||||
teamID, globalOrTeamID, titleID, fmt.Sprintf("installer-%d.pkg", i), fmt.Sprintf("v%d.0.0", i), scriptContentID,
|
||||
uninstallScriptContentID,
|
||||
hex.EncodeToString([]byte("test")), "darwin", i < 2)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -3469,7 +3493,9 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
swiIDs = append(swiIDs, uint(id))
|
||||
}
|
||||
// sw1Pending and swi2Installed are self-service installers
|
||||
swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm = swiIDs[0], swiIDs[1], swiIDs[2], swiIDs[3], swiIDs[4]
|
||||
swi1Pending, swi2Installed, swi3Failed, swi4Available, swi5Tm,
|
||||
swi6PendingUninstall, swi7FailedUninstall, swi8Uninstalled =
|
||||
swiIDs[0], swiIDs[1], swiIDs[2], swiIDs[3], swiIDs[4], swiIDs[5], swiIDs[6], swiIDs[7]
|
||||
|
||||
// create the results for the host
|
||||
|
||||
|
|
@ -3518,21 +3544,31 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
// swi5 is for another team
|
||||
_ = swi5Tm
|
||||
|
||||
// add another installer for a different platform, should be always omitted
|
||||
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('windows-title', 'programs')`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lid, _ := res.LastInsertId()
|
||||
// swi6 has been installed, and is pending uninstall
|
||||
_, err = q.ExecContext(ctx, `
|
||||
INSERT INTO software_installers
|
||||
(team_id, global_or_team_id, title_id, filename, version, install_script_content_id, storage_id, platform)
|
||||
VALUES
|
||||
(?, ?, ?, ?, ?, ?, unhex(?), ?)`,
|
||||
nil, 0, lid, "windows-installer-6.msi", "v6.0.0", scriptContentID, hex.EncodeToString([]byte("test")), "windows")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, pre_install_query_output, install_script_exit_code, post_install_script_exit_code)
|
||||
VALUES (?, ?, ?, ?, ?, ?)`,
|
||||
"uuid6-pre", host.ID, swi6PendingUninstall, "ok", 0, 0)
|
||||
require.NoError(t, err)
|
||||
_, err = q.ExecContext(ctx, `
|
||||
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, uninstall)
|
||||
VALUES (?, ?, ?, ?)`,
|
||||
"uuid6", host.ID, swi6PendingUninstall, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// swi7 is failed uninstall
|
||||
_, err = q.ExecContext(ctx, `
|
||||
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, uninstall, uninstall_script_exit_code)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
"uuid7", host.ID, swi7FailedUninstall, 1, 1)
|
||||
require.NoError(t, err)
|
||||
|
||||
// swi8 is successful uninstall
|
||||
_, err = q.ExecContext(ctx, `
|
||||
INSERT INTO host_software_installs (execution_id, host_id, software_installer_id, uninstall, uninstall_script_exit_code)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
"uuid8", host.ID, swi8Uninstalled, 1, 0)
|
||||
require.NoError(t, err)
|
||||
|
||||
return nil
|
||||
})
|
||||
|
|
@ -3541,7 +3577,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
expected[byNSV[b].Name+byNSV[b].Source] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "b",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"}},
|
||||
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
||||
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
|
||||
|
|
@ -3550,7 +3586,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
i0 := fleet.HostSoftwareWithInstaller{
|
||||
Name: "i0",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerInstalled),
|
||||
Status: expectStatus(fleet.SoftwareInstalled),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-1.pkg", Version: "v1.0.0", SelfService: ptr.Bool(true), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid2"}},
|
||||
}
|
||||
expected[i0.Name+i0.Source] = i0
|
||||
|
|
@ -3558,16 +3594,44 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
i1 := fleet.HostSoftwareWithInstaller{
|
||||
Name: "i1",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerFailed),
|
||||
Status: expectStatus(fleet.SoftwareInstallFailed),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid3"}},
|
||||
}
|
||||
expected[i1.Name+i1.Source] = i1
|
||||
|
||||
i4 := fleet.HostSoftwareWithInstaller{
|
||||
Name: "i4",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareUninstallPending),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-5.pkg", Version: "v5.0.0", SelfService: ptr.Bool(false),
|
||||
LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid6-pre"},
|
||||
LastUninstall: &fleet.HostSoftwareUninstall{ExecutionID: "uuid6"}},
|
||||
}
|
||||
expected[i4.Name+i4.Source] = i4
|
||||
|
||||
i5 := fleet.HostSoftwareWithInstaller{
|
||||
Name: "i5",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareUninstallFailed),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-6.pkg", Version: "v6.0.0", SelfService: ptr.Bool(false),
|
||||
LastUninstall: &fleet.HostSoftwareUninstall{ExecutionID: "uuid7"}},
|
||||
}
|
||||
expected[i5.Name+i5.Source] = i5
|
||||
|
||||
i6 := fleet.HostSoftwareWithInstaller{
|
||||
Name: "i6",
|
||||
Source: "apps",
|
||||
Status: nil,
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-7.pkg", Version: "v7.0.0", SelfService: ptr.Bool(false),
|
||||
LastUninstall: &fleet.HostSoftwareUninstall{ExecutionID: "uuid8"}},
|
||||
}
|
||||
expected[i6.Name+i6.Source] = i6
|
||||
|
||||
// request without available software
|
||||
opts.IncludeAvailableForInstall = false
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 7}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected))}, meta)
|
||||
compareResults(expected, sw, true)
|
||||
|
||||
// request with available software
|
||||
|
|
@ -3591,7 +3655,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
opts.ListOptions.PerPage = 20
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 8}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 1}, meta)
|
||||
compareResults(expected, sw, true, i3.Name+i3.Source)
|
||||
|
||||
// request with available software only (attempted to install and never attempted to install)
|
||||
|
|
@ -3600,6 +3664,9 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
expectedAvailableOnly[i0.Name+i0.Source] = i0
|
||||
expectedAvailableOnly[i1.Name+i1.Source] = i1
|
||||
expectedAvailableOnly[i2.Name+i2.Source] = i2
|
||||
expectedAvailableOnly[i4.Name+i4.Source] = i4
|
||||
expectedAvailableOnly[i5.Name+i5.Source] = i5
|
||||
expectedAvailableOnly[i6.Name+i6.Source] = i6
|
||||
opts.OnlyAvailableForInstall = true
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -3613,7 +3680,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
opts.IncludeAvailableForInstall = false
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 7}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 2}, meta)
|
||||
compareResults(expected, sw, false, i2.Name+i2.Source, i3.Name+i3.Source)
|
||||
opts.ListOptions.OrderDirection = fleet.OrderAscending
|
||||
opts.ListOptions.TestSecondaryOrderDirection = fleet.OrderAscending
|
||||
|
|
@ -3642,7 +3709,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
expected[byNSV[b].Name+byNSV[b].Source] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "b",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerFailed),
|
||||
Status: expectStatus(fleet.SoftwareInstallFailed),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-0.pkg", Version: "v0.0.0", SelfService: ptr.Bool(true), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid1"}},
|
||||
InstalledVersions: []*fleet.HostSoftwareInstalledVersion{
|
||||
{Version: byNSV[b].Version, Vulnerabilities: []string{vulns[3].CVE}, InstalledPaths: []string{installPaths[2]}},
|
||||
|
|
@ -3651,7 +3718,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
expected[i1.Name+i1.Source] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "i1",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: "uuid4"}},
|
||||
}
|
||||
expectedAvailableOnly[byNSV[b].Name+byNSV[b].Source] = expected[byNSV[b].Name+byNSV[b].Source]
|
||||
|
|
@ -3661,14 +3728,14 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
opts.IncludeAvailableForInstall = false
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 7}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 2}, meta)
|
||||
compareResults(expected, sw, true, i2.Name+i2.Source, i3.Name+i3.Source)
|
||||
|
||||
// request with available software)
|
||||
opts.IncludeAvailableForInstall = true
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 8}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 1}, meta)
|
||||
compareResults(expected, sw, true, i3.Name+i3.Source)
|
||||
|
||||
// create a new host in the team, with no software
|
||||
|
|
@ -3775,13 +3842,13 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
expected["vpp1apps"] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "vpp1",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerInstalled),
|
||||
Status: expectStatus(fleet.SoftwareInstalled),
|
||||
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1CmdUUID}},
|
||||
}
|
||||
expected["vpp2apps"] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "vpp2",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2, SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp2bCmdUUID}},
|
||||
}
|
||||
|
||||
|
|
@ -3789,7 +3856,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
opts.ListOptions.MatchQuery = ""
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 9}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 2}, meta)
|
||||
compareResults(expected, sw, true, i3.Name+i3.Source, i2.Name+i2.Source) // i3 is for team, i2 is available (excluded)
|
||||
|
||||
expected["vpp3apps"] = fleet.HostSoftwareWithInstaller{
|
||||
|
|
@ -3805,7 +3872,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
opts.ListOptions.PerPage = 20
|
||||
sw, meta, err = ds.ListHostSoftware(ctx, host, opts)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: 11}, meta)
|
||||
require.Equal(t, &fleet.PaginationMetadata{TotalResults: uint(len(expected)) - 1}, meta)
|
||||
compareResults(expected, sw, true, i3.Name+i3.Source) // i3 is for team
|
||||
|
||||
// Available for install only
|
||||
|
|
@ -3826,7 +3893,7 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
"vpp1apps": {
|
||||
Name: "vpp1",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1TmCmdUUID}},
|
||||
},
|
||||
}, sw, true)
|
||||
|
|
@ -3847,13 +3914,13 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
"i1apps": {
|
||||
Name: "i1",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-2.pkg", Version: "v2.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI1UUID}},
|
||||
},
|
||||
"i2apps": {
|
||||
Name: "i2",
|
||||
Source: "apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{Name: "installer-3.pkg", Version: "v3.0.0", SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{InstallUUID: otherHostI2UUID}},
|
||||
},
|
||||
}
|
||||
|
|
@ -3868,37 +3935,40 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 3}, IncludeAvailableForInstall: false},
|
||||
wantNames: []string{byNSV[a1].Name, byNSV[a2].Name, byNSV[b].Name},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 9},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 12},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 3}, IncludeAvailableForInstall: false},
|
||||
wantNames: []string{byNSV[c1].Name, byNSV[d].Name, i0.Name},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 9},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 12},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 2, PerPage: 3}, IncludeAvailableForInstall: false},
|
||||
wantNames: []string{i1.Name, "vpp1", "vpp2"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 9},
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 3, PerPage: 3},
|
||||
IncludeAvailableForInstall: false},
|
||||
wantNames: []string{i6.Name, "vpp1", "vpp2"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 12},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 3, PerPage: 3}, IncludeAvailableForInstall: false},
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 4, PerPage: 3},
|
||||
IncludeAvailableForInstall: false},
|
||||
wantNames: []string{},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 9},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 12},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 4}, IncludeAvailableForInstall: true},
|
||||
wantNames: []string{byNSV[a1].Name, byNSV[a2].Name, byNSV[b].Name, byNSV[c1].Name},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 11},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 14},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 4}, IncludeAvailableForInstall: true},
|
||||
wantNames: []string{byNSV[d].Name, i0.Name, i1.Name, i2.Name},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 11},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true, TotalResults: 14},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 2, PerPage: 4}, IncludeAvailableForInstall: true},
|
||||
wantNames: []string{"vpp1", "vpp2", "vpp3"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 11},
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 3, PerPage: 4},
|
||||
IncludeAvailableForInstall: true},
|
||||
wantNames: []string{"vpp2", "vpp3"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 14},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{PerPage: 3}, IncludeAvailableForInstall: true, SelfServiceOnly: true},
|
||||
|
|
@ -3913,12 +3983,13 @@ func testListHostSoftware(t *testing.T, ds *Datastore) {
|
|||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 0, PerPage: 4}, OnlyAvailableForInstall: true},
|
||||
wantNames: []string{byNSV[b].Name, "i0", "i1", "i2"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 7},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: false, TotalResults: 10},
|
||||
},
|
||||
{
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 1, PerPage: 4}, OnlyAvailableForInstall: true},
|
||||
wantNames: []string{"vpp1", "vpp2", "vpp3"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 7},
|
||||
opts: fleet.HostSoftwareTitleListOptions{ListOptions: fleet.ListOptions{Page: 2, PerPage: 4},
|
||||
OnlyAvailableForInstall: true},
|
||||
wantNames: []string{"vpp2", "vpp3"},
|
||||
wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 10},
|
||||
},
|
||||
}
|
||||
for _, c := range cases {
|
||||
|
|
@ -4204,13 +4275,13 @@ func testListIOSHostSoftware(t *testing.T, ds *Datastore) {
|
|||
expected["vpp1ios_apps"] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "vpp1",
|
||||
Source: "ios_apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerInstalled),
|
||||
Status: expectStatus(fleet.SoftwareInstalled),
|
||||
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp1, SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp1CmdUUID}},
|
||||
}
|
||||
expected["vpp2ios_apps"] = fleet.HostSoftwareWithInstaller{
|
||||
Name: "vpp2",
|
||||
Source: "ios_apps",
|
||||
Status: expectStatus(fleet.SoftwareInstallerPending),
|
||||
Status: expectStatus(fleet.SoftwareInstallPending),
|
||||
AppStoreApp: &fleet.SoftwarePackageOrApp{AppStoreID: vpp2, SelfService: ptr.Bool(false), LastInstall: &fleet.HostSoftwareInstall{CommandUUID: vpp2bCmdUUID}},
|
||||
}
|
||||
|
||||
|
|
@ -4267,6 +4338,14 @@ func testSetHostSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
scriptContentID, _ := res.LastInsertId()
|
||||
|
||||
uninstallScript := `echo 'bar'`
|
||||
resUninstall, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`,
|
||||
uninstallScript, uninstallScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uninstallScriptContentID, _ := resUninstall.LastInsertId()
|
||||
|
||||
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('foo', 'apps')`)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -4275,10 +4354,10 @@ func testSetHostSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
|
||||
res, err = q.ExecContext(ctx, `
|
||||
INSERT INTO software_installers
|
||||
(title_id, filename, version, install_script_content_id, storage_id)
|
||||
(title_id, filename, version, install_script_content_id, uninstall_script_content_id, storage_id)
|
||||
VALUES
|
||||
(?, ?, ?, ?, unhex(?))`,
|
||||
titleID, "installer.pkg", "v1.0.0", scriptContentID, hex.EncodeToString([]byte("test")))
|
||||
(?, ?, ?, ?, ?, unhex(?))`,
|
||||
titleID, "installer.pkg", "v1.0.0", scriptContentID, uninstallScriptContentID, hex.EncodeToString([]byte("test")))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -495,6 +495,7 @@ func CreateNamedMySQLDS(t *testing.T, name string) *Datastore {
|
|||
}
|
||||
|
||||
func ExecAdhocSQL(tb testing.TB, ds *Datastore, fn func(q sqlx.ExtContext) error) {
|
||||
tb.Helper()
|
||||
err := fn(ds.primary)
|
||||
require.NoError(tb, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,9 +101,9 @@ WHERE
|
|||
"mdm_status_acknowledged": fleet.MDMAppleStatusAcknowledged,
|
||||
"mdm_status_error": fleet.MDMAppleStatusError,
|
||||
"mdm_status_format_error": fleet.MDMAppleStatusCommandFormatError,
|
||||
"software_status_pending": fleet.SoftwareInstallerPending,
|
||||
"software_status_failed": fleet.SoftwareInstallerFailed,
|
||||
"software_status_installed": fleet.SoftwareInstallerInstalled,
|
||||
"software_status_pending": fleet.SoftwareInstallPending,
|
||||
"software_status_failed": fleet.SoftwareInstallFailed,
|
||||
"software_status_installed": fleet.SoftwareInstalled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get summary host vpp installs: named query")
|
||||
|
|
@ -519,8 +519,8 @@ WHERE
|
|||
|
||||
listStmt, args, err := sqlx.Named(stmt, map[string]any{
|
||||
"command_uuid": commandResults.CommandUUID,
|
||||
"software_status_failed": string(fleet.SoftwareInstallerFailed),
|
||||
"software_status_installed": string(fleet.SoftwareInstallerInstalled),
|
||||
"software_status_failed": string(fleet.SoftwareInstallFailed),
|
||||
"software_status_installed": string(fleet.SoftwareInstalled),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "build list query from named args")
|
||||
|
|
@ -547,14 +547,14 @@ WHERE
|
|||
var status string
|
||||
switch commandResults.Status {
|
||||
case fleet.MDMAppleStatusAcknowledged:
|
||||
status = string(fleet.SoftwareInstallerInstalled)
|
||||
status = string(fleet.SoftwareInstalled)
|
||||
case fleet.MDMAppleStatusCommandFormatError:
|
||||
case fleet.MDMAppleStatusError:
|
||||
status = string(fleet.SoftwareInstallerFailed)
|
||||
status = string(fleet.SoftwareInstallFailed)
|
||||
default:
|
||||
// This case shouldn't happen (we should only be doing this check if the command is in a
|
||||
// "terminal" state, but adding it so we have a default
|
||||
status = string(fleet.SoftwareInstallerPending)
|
||||
status = string(fleet.SoftwareInstallPending)
|
||||
}
|
||||
|
||||
act := &fleet.ActivityInstalledAppStoreApp{
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ var ActivityDetailsList = []ActivityDetails{
|
|||
ActivityTypeResentConfigurationProfile{},
|
||||
|
||||
ActivityTypeInstalledSoftware{},
|
||||
ActivityTypeUninstalledSoftware{},
|
||||
ActivityTypeAddedSoftware{},
|
||||
ActivityTypeDeletedSoftware{},
|
||||
ActivityEnabledVPP{},
|
||||
|
|
@ -1521,6 +1522,38 @@ func (a ActivityTypeInstalledSoftware) Documentation() (activity, details, detai
|
|||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeUninstalledSoftware struct {
|
||||
HostID uint `json:"host_id"`
|
||||
HostDisplayName string `json:"host_display_name"`
|
||||
SoftwareTitle string `json:"software_title"`
|
||||
ExecutionID string `json:"script_execution_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
func (a ActivityTypeUninstalledSoftware) ActivityName() string {
|
||||
return "uninstalled_software"
|
||||
}
|
||||
|
||||
func (a ActivityTypeUninstalledSoftware) HostIDs() []uint {
|
||||
return []uint{a.HostID}
|
||||
}
|
||||
|
||||
func (a ActivityTypeUninstalledSoftware) Documentation() (activity, details, detailsExample string) {
|
||||
return `Generated when a software is uninstalled on a host.`,
|
||||
`This activity contains the following fields:
|
||||
- "host_id": ID of the host.
|
||||
- "host_display_name": Display name of the host.
|
||||
- "software_title": Name of the software.
|
||||
- "script_execution_id": ID of the software uninstall script.
|
||||
- "status": Status of the software uninstallation.`, `{
|
||||
"host_id": 1,
|
||||
"host_display_name": "Anna's MacBook Pro",
|
||||
"software_title": "Falcon.app",
|
||||
"script_execution_id": "ece8d99d-4313-446a-9af2-e152cd1bad1e",
|
||||
"status": "uninstalled"
|
||||
}`
|
||||
}
|
||||
|
||||
type ActivityTypeAddedSoftware struct {
|
||||
SoftwareTitle string `json:"software_title"`
|
||||
SoftwarePackage string `json:"software_package"`
|
||||
|
|
|
|||
|
|
@ -532,6 +532,11 @@ type Datastore interface {
|
|||
// software installer in the host. It returns the auto-generated installation
|
||||
// uuid.
|
||||
InsertSoftwareInstallRequest(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool) (string, error)
|
||||
// InsertSoftwareUninstallRequest tracks a new request to uninstall the provided
|
||||
// software installer on the host. executionID is the script execution ID corresponding to uninstall script
|
||||
InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error
|
||||
// GetSoftwareTitleNameFromExecutionID returns the software title name associated with the provided software install execution ID.
|
||||
GetSoftwareTitleNameFromExecutionID(ctx context.Context, executionID string) (string, error)
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// SoftwareStore
|
||||
|
|
@ -1542,8 +1547,8 @@ type Datastore interface {
|
|||
// SetHostScriptExecutionResult stores the result of a host script execution
|
||||
// and returns the updated host script result record. Note that it does not
|
||||
// fail if the script execution request does not exist, in this case it will
|
||||
// return nil, nil.
|
||||
SetHostScriptExecutionResult(ctx context.Context, result *HostScriptResultPayload) (*HostScriptResult, error)
|
||||
// return nil, "", nil. action is populated if this script was an MDM action (lock/unlock/wipe/uninstall).
|
||||
SetHostScriptExecutionResult(ctx context.Context, result *HostScriptResultPayload) (hsr *HostScriptResult, action string, err error)
|
||||
// GetHostScriptExecutionResult returns the result of a host script
|
||||
// execution. It returns the host script results even if no results have been
|
||||
// received, it is the caller's responsibility to check if that was the case
|
||||
|
|
@ -1563,6 +1568,10 @@ type Datastore interface {
|
|||
// script.
|
||||
GetScriptContents(ctx context.Context, id uint) ([]byte, error)
|
||||
|
||||
// GetAnyScriptContents returns the raw script contents of the corresponding
|
||||
// script, regardless whether it is present in the scripts table.
|
||||
GetAnyScriptContents(ctx context.Context, id uint) ([]byte, error)
|
||||
|
||||
// DeleteScript deletes the script identified by its id.
|
||||
DeleteScript(ctx context.Context, id uint) error
|
||||
|
||||
|
|
|
|||
|
|
@ -637,6 +637,9 @@ type Service interface {
|
|||
// InstallSoftwareTitle installs a software title in the given host.
|
||||
InstallSoftwareTitle(ctx context.Context, hostID uint, softwareTitleID uint) error
|
||||
|
||||
// UninstallSoftwareTitle uninstalls a software title in the given host.
|
||||
UninstallSoftwareTitle(ctx context.Context, hostID uint, softwareTitleID uint) error
|
||||
|
||||
// GetSoftwareInstallResults gets the results for a particular software install attempt.
|
||||
GetSoftwareInstallResults(ctx context.Context, installUUID string) (*HostSoftwareInstallerResult, error)
|
||||
|
||||
|
|
|
|||
|
|
@ -89,10 +89,14 @@ type SoftwareInstaller struct {
|
|||
InstallScript string `json:"install_script" db:"install_script"`
|
||||
// InstallScriptContentID is the ID of the install script content.
|
||||
InstallScriptContentID uint `json:"-" db:"install_script_content_id"`
|
||||
// UninstallScriptContentID is the ID of the uninstall script content.
|
||||
UninstallScriptContentID uint `json:"-" db:"uninstall_script_content_id"`
|
||||
// PreInstallQuery is the query to run as a condition to installing the software package.
|
||||
PreInstallQuery string `json:"pre_install_query" db:"pre_install_query"`
|
||||
// PostInstallScript is the script to run after installing the software package.
|
||||
PostInstallScript string `json:"post_install_script" db:"post_install_script"`
|
||||
// UninstallScript is the script to run to uninstall the software package.
|
||||
UninstallScript string `json:"uninstall_script" db:"uninstall_script"`
|
||||
// PostInstallScriptContentID is the ID of the post-install script content.
|
||||
PostInstallScriptContentID *uint `json:"-" db:"post_install_script_content_id"`
|
||||
// StorageID is the unique identifier for the software package in the software installer store.
|
||||
|
|
@ -117,27 +121,40 @@ func (s *SoftwareInstaller) AuthzType() string {
|
|||
type SoftwareInstallerStatusSummary struct {
|
||||
// Installed is the number of hosts that have the software package installed.
|
||||
Installed uint `json:"installed" db:"installed"`
|
||||
// Pending is the number of hosts that have the software package pending installation.
|
||||
Pending uint `json:"pending" db:"pending"`
|
||||
// Failed is the number of hosts that have the software package installation failed.
|
||||
Failed uint `json:"failed" db:"failed"`
|
||||
// PendingInstall is the number of hosts that have the software package pending installation.
|
||||
PendingInstall uint `json:"pending_install" db:"pending_install"`
|
||||
// FailedInstall is the number of hosts that have the software package installation failed.
|
||||
FailedInstall uint `json:"failed_install" db:"failed_install"`
|
||||
// PendingUninstall is the number of hosts that have the software package pending installation.
|
||||
PendingUninstall uint `json:"pending_uninstall" db:"pending_uninstall"`
|
||||
// FailedInstall is the number of hosts that have the software package installation failed.
|
||||
FailedUninstall uint `json:"failed_uninstall" db:"failed_uninstall"`
|
||||
}
|
||||
|
||||
// SoftwareInstallerStatus represents the status of a software installer package on a host.
|
||||
type SoftwareInstallerStatus string
|
||||
|
||||
const (
|
||||
SoftwareInstallerPending SoftwareInstallerStatus = "pending"
|
||||
SoftwareInstallerFailed SoftwareInstallerStatus = "failed"
|
||||
SoftwareInstallerInstalled SoftwareInstallerStatus = "installed"
|
||||
SoftwareInstallPending SoftwareInstallerStatus = "pending_install"
|
||||
SoftwareInstallFailed SoftwareInstallerStatus = "failed_install"
|
||||
SoftwareInstalled SoftwareInstallerStatus = "installed"
|
||||
SoftwareUninstallPending SoftwareInstallerStatus = "pending_uninstall"
|
||||
SoftwareUninstallFailed SoftwareInstallerStatus = "failed_uninstall"
|
||||
// SoftwarePending and SoftwareFailed statuses are only used as filters in the API and are not stored in the database.
|
||||
SoftwarePending SoftwareInstallerStatus = "pending" // either pending_install or pending_uninstall
|
||||
SoftwareFailed SoftwareInstallerStatus = "failed" // either failed_install or failed_uninstall
|
||||
)
|
||||
|
||||
func (s SoftwareInstallerStatus) IsValid() bool {
|
||||
switch s {
|
||||
case
|
||||
SoftwareInstallerFailed,
|
||||
SoftwareInstallerInstalled,
|
||||
SoftwareInstallerPending:
|
||||
SoftwarePending,
|
||||
SoftwareFailed,
|
||||
SoftwareUninstallPending,
|
||||
SoftwareUninstallFailed,
|
||||
SoftwareInstallFailed,
|
||||
SoftwareInstalled,
|
||||
SoftwareInstallPending:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
|
@ -222,7 +239,7 @@ Rolled back successfully
|
|||
// EnhanceOutputDetails is used to add extra boilerplate/information to the
|
||||
// output fields so they're easier to consume by users.
|
||||
func (h *HostSoftwareInstallerResult) EnhanceOutputDetails() {
|
||||
if h.Status == SoftwareInstallerPending {
|
||||
if h.Status == SoftwareInstallPending {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -282,6 +299,9 @@ type UploadSoftwareInstallerPayload struct {
|
|||
SelfService bool
|
||||
UserID uint
|
||||
URL string
|
||||
PackageIDs []string
|
||||
UninstallScript string
|
||||
Extension string
|
||||
}
|
||||
|
||||
// DownloadSoftwareInstallerPayload is the payload for downloading a software installer.
|
||||
|
|
@ -349,11 +369,12 @@ type SoftwarePackageOrApp struct {
|
|||
// Name is only present for software installer packages.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
Version string `json:"version"`
|
||||
SelfService *bool `json:"self_service,omitempty"`
|
||||
IconURL *string `json:"icon_url"`
|
||||
LastInstall *HostSoftwareInstall `json:"last_install"`
|
||||
PackageURL *string `json:"package_url"`
|
||||
Version string `json:"version"`
|
||||
SelfService *bool `json:"self_service,omitempty"`
|
||||
IconURL *string `json:"icon_url"`
|
||||
LastInstall *HostSoftwareInstall `json:"last_install"`
|
||||
LastUninstall *HostSoftwareUninstall `json:"last_uninstall"`
|
||||
PackageURL *string `json:"package_url"`
|
||||
}
|
||||
|
||||
type SoftwarePackageSpec struct {
|
||||
|
|
@ -384,6 +405,14 @@ type HostSoftwareInstall struct {
|
|||
InstalledAt time.Time `json:"installed_at"`
|
||||
}
|
||||
|
||||
// HostSoftwareUninstall represents uninstallation of software from a host with a
|
||||
// Fleet software installer.
|
||||
type HostSoftwareUninstall struct {
|
||||
// ExecutionID is the UUID of the script execution that uninstalled the software.
|
||||
ExecutionID string `json:"script_execution_id,omitempty"`
|
||||
UninstalledAt time.Time `json:"uninstalled_at"`
|
||||
}
|
||||
|
||||
// HostSoftwareInstalledVersion represents a version of software installed on a
|
||||
// host.
|
||||
type HostSoftwareInstalledVersion struct {
|
||||
|
|
@ -417,17 +446,17 @@ type HostSoftwareInstallResultPayload struct {
|
|||
func (h *HostSoftwareInstallResultPayload) Status() SoftwareInstallerStatus {
|
||||
switch {
|
||||
case h.PostInstallScriptExitCode != nil && *h.PostInstallScriptExitCode == 0:
|
||||
return SoftwareInstallerInstalled
|
||||
return SoftwareInstalled
|
||||
case h.PostInstallScriptExitCode != nil && *h.PostInstallScriptExitCode != 0:
|
||||
return SoftwareInstallerFailed
|
||||
return SoftwareInstallFailed
|
||||
case h.InstallScriptExitCode != nil && *h.InstallScriptExitCode == 0:
|
||||
return SoftwareInstallerInstalled
|
||||
return SoftwareInstalled
|
||||
case h.InstallScriptExitCode != nil && *h.InstallScriptExitCode != 0:
|
||||
return SoftwareInstallerFailed
|
||||
return SoftwareInstallFailed
|
||||
case h.PreInstallConditionOutput != nil && *h.PreInstallConditionOutput == "":
|
||||
return SoftwareInstallerFailed
|
||||
return SoftwareInstallFailed
|
||||
default:
|
||||
return SoftwareInstallerPending
|
||||
return SoftwareInstallPending
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "pending status",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerPending,
|
||||
Status: SoftwareInstallPending,
|
||||
},
|
||||
expectedPreInstallQueryOutput: nil,
|
||||
expectedOutput: nil,
|
||||
|
|
@ -98,7 +98,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with empty PreInstallQueryOutput",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
PreInstallQueryOutput: ptr.String(""),
|
||||
},
|
||||
expectedPreInstallQueryOutput: ptr.String(SoftwareInstallerQueryFailCopy),
|
||||
|
|
@ -108,7 +108,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with non-empty PreInstallQueryOutput",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
PreInstallQueryOutput: ptr.String("Some output"),
|
||||
},
|
||||
expectedPreInstallQueryOutput: ptr.String(SoftwareInstallerQuerySuccessCopy),
|
||||
|
|
@ -118,7 +118,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with nil PreInstallQueryOutput",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
},
|
||||
expectedPreInstallQueryOutput: nil,
|
||||
expectedOutput: nil,
|
||||
|
|
@ -127,7 +127,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with install scripts disabled",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
InstallScriptExitCode: ptr.Int(-2),
|
||||
Output: ptr.String(""),
|
||||
},
|
||||
|
|
@ -138,7 +138,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with failed install script",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerFailed,
|
||||
Status: SoftwareInstallFailed,
|
||||
InstallScriptExitCode: ptr.Int(1),
|
||||
Output: ptr.String("Some install output"),
|
||||
},
|
||||
|
|
@ -149,7 +149,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with successful install script",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
InstallScriptExitCode: ptr.Int(0),
|
||||
Output: ptr.String("Some install output"),
|
||||
},
|
||||
|
|
@ -160,7 +160,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with successful post install script",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
InstallScriptExitCode: ptr.Int(0),
|
||||
Output: ptr.String("Some install output"),
|
||||
PostInstallScriptExitCode: ptr.Int(0),
|
||||
|
|
@ -173,7 +173,7 @@ func TestEnhanceOutputDetails(t *testing.T) {
|
|||
{
|
||||
name: "non-pending status with failed post install script",
|
||||
initial: HostSoftwareInstallerResult{
|
||||
Status: SoftwareInstallerInstalled,
|
||||
Status: SoftwareInstalled,
|
||||
InstallScriptExitCode: ptr.Int(0),
|
||||
Output: ptr.String("Some install output"),
|
||||
PostInstallScriptExitCode: ptr.Int(1),
|
||||
|
|
|
|||
|
|
@ -398,6 +398,10 @@ type SoftwareTitleByIDFunc func(ctx context.Context, id uint, teamID *uint, tmFi
|
|||
|
||||
type InsertSoftwareInstallRequestFunc func(ctx context.Context, hostID uint, softwareInstallerID uint, selfService bool) (string, error)
|
||||
|
||||
type InsertSoftwareUninstallRequestFunc func(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error
|
||||
|
||||
type GetSoftwareTitleNameFromExecutionIDFunc func(ctx context.Context, executionID string) (string, error)
|
||||
|
||||
type ListSoftwareForVulnDetectionFunc func(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error)
|
||||
|
||||
type ListSoftwareVulnerabilitiesByHostIDsSourceFunc func(ctx context.Context, hostIDs []uint, source fleet.VulnerabilitySource) (map[uint][]fleet.SoftwareVulnerability, error)
|
||||
|
|
@ -982,7 +986,7 @@ type SetOrUpdateMDMAppleDeclarationFunc func(ctx context.Context, declaration *f
|
|||
|
||||
type NewHostScriptExecutionRequestFunc func(ctx context.Context, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error)
|
||||
|
||||
type SetHostScriptExecutionResultFunc func(ctx context.Context, result *fleet.HostScriptResultPayload) (*fleet.HostScriptResult, error)
|
||||
type SetHostScriptExecutionResultFunc func(ctx context.Context, result *fleet.HostScriptResultPayload) (hsr *fleet.HostScriptResult, action string, err error)
|
||||
|
||||
type GetHostScriptExecutionResultFunc func(ctx context.Context, execID string) (*fleet.HostScriptResult, error)
|
||||
|
||||
|
|
@ -994,6 +998,8 @@ type ScriptFunc func(ctx context.Context, id uint) (*fleet.Script, error)
|
|||
|
||||
type GetScriptContentsFunc func(ctx context.Context, id uint) ([]byte, error)
|
||||
|
||||
type GetAnyScriptContentsFunc func(ctx context.Context, id uint) ([]byte, error)
|
||||
|
||||
type DeleteScriptFunc func(ctx context.Context, id uint) error
|
||||
|
||||
type ListScriptsFunc func(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]*fleet.Script, *fleet.PaginationMetadata, error)
|
||||
|
|
@ -1638,6 +1644,12 @@ type DataStore struct {
|
|||
InsertSoftwareInstallRequestFunc InsertSoftwareInstallRequestFunc
|
||||
InsertSoftwareInstallRequestFuncInvoked bool
|
||||
|
||||
InsertSoftwareUninstallRequestFunc InsertSoftwareUninstallRequestFunc
|
||||
InsertSoftwareUninstallRequestFuncInvoked bool
|
||||
|
||||
GetSoftwareTitleNameFromExecutionIDFunc GetSoftwareTitleNameFromExecutionIDFunc
|
||||
GetSoftwareTitleNameFromExecutionIDFuncInvoked bool
|
||||
|
||||
ListSoftwareForVulnDetectionFunc ListSoftwareForVulnDetectionFunc
|
||||
ListSoftwareForVulnDetectionFuncInvoked bool
|
||||
|
||||
|
|
@ -2532,6 +2544,9 @@ type DataStore struct {
|
|||
GetScriptContentsFunc GetScriptContentsFunc
|
||||
GetScriptContentsFuncInvoked bool
|
||||
|
||||
GetAnyScriptContentsFunc GetAnyScriptContentsFunc
|
||||
GetAnyScriptContentsFuncInvoked bool
|
||||
|
||||
DeleteScriptFunc DeleteScriptFunc
|
||||
DeleteScriptFuncInvoked bool
|
||||
|
||||
|
|
@ -3972,6 +3987,20 @@ func (s *DataStore) InsertSoftwareInstallRequest(ctx context.Context, hostID uin
|
|||
return s.InsertSoftwareInstallRequestFunc(ctx, hostID, softwareInstallerID, selfService)
|
||||
}
|
||||
|
||||
func (s *DataStore) InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint) error {
|
||||
s.mu.Lock()
|
||||
s.InsertSoftwareUninstallRequestFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.InsertSoftwareUninstallRequestFunc(ctx, executionID, hostID, softwareInstallerID)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetSoftwareTitleNameFromExecutionID(ctx context.Context, executionID string) (string, error) {
|
||||
s.mu.Lock()
|
||||
s.GetSoftwareTitleNameFromExecutionIDFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetSoftwareTitleNameFromExecutionIDFunc(ctx, executionID)
|
||||
}
|
||||
|
||||
func (s *DataStore) ListSoftwareForVulnDetection(ctx context.Context, filter fleet.VulnSoftwareFilter) ([]fleet.Software, error) {
|
||||
s.mu.Lock()
|
||||
s.ListSoftwareForVulnDetectionFuncInvoked = true
|
||||
|
|
@ -6016,7 +6045,7 @@ func (s *DataStore) NewHostScriptExecutionRequest(ctx context.Context, request *
|
|||
return s.NewHostScriptExecutionRequestFunc(ctx, request)
|
||||
}
|
||||
|
||||
func (s *DataStore) SetHostScriptExecutionResult(ctx context.Context, result *fleet.HostScriptResultPayload) (*fleet.HostScriptResult, error) {
|
||||
func (s *DataStore) SetHostScriptExecutionResult(ctx context.Context, result *fleet.HostScriptResultPayload) (hsr *fleet.HostScriptResult, action string, err error) {
|
||||
s.mu.Lock()
|
||||
s.SetHostScriptExecutionResultFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
|
|
@ -6058,6 +6087,13 @@ func (s *DataStore) GetScriptContents(ctx context.Context, id uint) ([]byte, err
|
|||
return s.GetScriptContentsFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetAnyScriptContents(ctx context.Context, id uint) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
s.GetAnyScriptContentsFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetAnyScriptContentsFunc(ctx, id)
|
||||
}
|
||||
|
||||
func (s *DataStore) DeleteScript(ctx context.Context, id uint) error {
|
||||
s.mu.Lock()
|
||||
s.DeleteScriptFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -367,7 +367,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
|
||||
ue.GET("/api/_version_/fleet/software/titles", listSoftwareTitlesEndpoint, listSoftwareTitlesRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/titles/{id:[0-9]+}", getSoftwareTitleEndpoint, getSoftwareTitleRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/software/install/{software_title_id:[0-9]+}", installSoftwareTitleEndpoint, installSoftwareRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/software/{software_title_id:[0-9]+}/install", installSoftwareTitleEndpoint,
|
||||
installSoftwareRequest{})
|
||||
ue.POST("/api/_version_/fleet/hosts/{host_id:[0-9]+}/software/{software_title_id:[0-9]+}/uninstall", uninstallSoftwareTitleEndpoint,
|
||||
uninstallSoftwareRequest{})
|
||||
|
||||
// Sofware installers
|
||||
ue.GET("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/package", getSoftwareInstallerEndpoint, getSoftwareInstallerRequest{})
|
||||
|
|
@ -375,7 +378,8 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
getSoftwareInstallerRequest{})
|
||||
ue.POST("/api/_version_/fleet/software/package", uploadSoftwareInstallerEndpoint, uploadSoftwareInstallerRequest{})
|
||||
ue.DELETE("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/available_for_install", deleteSoftwareInstallerEndpoint, deleteSoftwareInstallerRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/install/results/{install_uuid}", getSoftwareInstallResultsEndpoint, getSoftwareInstallResultsRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/install/{install_uuid}/results", getSoftwareInstallResultsEndpoint,
|
||||
getSoftwareInstallResultsRequest{})
|
||||
ue.POST("/api/_version_/fleet/software/batch", batchSetSoftwareInstallersEndpoint, batchSetSoftwareInstallersRequest{})
|
||||
|
||||
// App store software
|
||||
|
|
|
|||
|
|
@ -5906,7 +5906,7 @@ func (s *integrationEnterpriseTestSuite) TestRunHostScript() {
|
|||
case r := <-resultsCh:
|
||||
r.ExecutionID = pending[0].ExecutionID
|
||||
// ignoring errors in this goroutine, the HTTP request below will fail if this fails
|
||||
_, err = s.ds.SetHostScriptExecutionResult(ctx, r)
|
||||
_, _, err = s.ds.SetHostScriptExecutionResult(ctx, r)
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
|
|
@ -9960,7 +9960,7 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
|
|||
|
||||
// request installation on the host
|
||||
var installResp installSoftwareResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d",
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install",
|
||||
host.ID, titleID), nil, http.StatusAccepted, &installResp)
|
||||
|
||||
// still returned by user-authenticated endpoint, now pending
|
||||
|
|
@ -9974,7 +9974,7 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
|
|||
require.NotNil(t, getHostSw.Software[2].SoftwarePackage)
|
||||
require.Equal(t, "ruby.deb", getHostSw.Software[2].SoftwarePackage.Name)
|
||||
require.NotNil(t, getHostSw.Software[2].Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *getHostSw.Software[2].Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *getHostSw.Software[2].Status)
|
||||
require.NotNil(t, getHostSw.Software[2].SoftwarePackage.SelfService)
|
||||
require.True(t, *getHostSw.Software[2].SoftwarePackage.SelfService)
|
||||
|
||||
|
|
@ -9995,7 +9995,7 @@ func (s *integrationEnterpriseTestSuite) TestListHostSoftware() {
|
|||
require.Equal(t, getDeviceSw.Software[2].Name, "ruby")
|
||||
require.Len(t, getDeviceSw.Software[1].InstalledVersions, 2)
|
||||
require.NotNil(t, getDeviceSw.Software[2].Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *getDeviceSw.Software[2].Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *getDeviceSw.Software[2].Status)
|
||||
require.NotNil(t, getDeviceSw.Software[2].SoftwarePackage)
|
||||
require.Nil(t, getDeviceSw.Software[2].AppStoreApp)
|
||||
require.NotNil(t, getDeviceSw.Software[2].SoftwarePackage.SelfService)
|
||||
|
|
@ -10866,6 +10866,14 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerNewInstallRequestP
|
|||
}
|
||||
scriptContentID, _ := res.LastInsertId()
|
||||
|
||||
uninstallScript := fmt.Sprintf(`echo uninstall '%s'`, kind)
|
||||
resUninstall, err := q.ExecContext(ctx, `INSERT INTO script_contents (md5_checksum, contents) VALUES (UNHEX(md5(?)), ?)`,
|
||||
uninstallScript, uninstallScript)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uninstallScriptContentID, _ := resUninstall.LastInsertId()
|
||||
|
||||
res, err = q.ExecContext(ctx, `INSERT INTO software_titles (name, source) VALUES ('foo', ?)`, kind)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -10875,10 +10883,11 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerNewInstallRequestP
|
|||
|
||||
_, err = q.ExecContext(ctx, `
|
||||
INSERT INTO software_installers
|
||||
(title_id, filename, version, install_script_content_id, storage_id, team_id, global_or_team_id, pre_install_query)
|
||||
(title_id, filename, version, install_script_content_id, uninstall_script_content_id, storage_id, team_id, global_or_team_id, pre_install_query)
|
||||
VALUES
|
||||
(?, ?, ?, ?, unhex(?), ?, ?, ?)`,
|
||||
titleID, fmt.Sprintf("installer.%s", kind), "v1.0.0", scriptContentID, hex.EncodeToString([]byte("test")), tm.ID, tm.ID, "foo")
|
||||
(?, ?, ?, ?, ?, unhex(?), ?, ?, ?)`,
|
||||
titleID, fmt.Sprintf("installer.%s", kind), "v1.0.0", scriptContentID, uninstallScriptContentID,
|
||||
hex.EncodeToString([]byte("test")), tm.ID, tm.ID, "foo")
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
|
@ -10901,7 +10910,8 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerNewInstallRequestP
|
|||
}
|
||||
|
||||
var resp installSoftwareResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", host.ID, softwareTitles[kind]), nil, wantStatus, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", host.ID, softwareTitles[kind]), nil,
|
||||
wantStatus, &resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10927,7 +10937,8 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
|
||||
var resp installSoftwareResponse
|
||||
// non-existent host
|
||||
s.DoJSON("POST", "/api/latest/fleet/hosts/1/software/install/1", nil, http.StatusNotFound, &resp)
|
||||
s.DoJSON("POST", "/api/latest/fleet/hosts/1/software/1/install", nil, http.StatusNotFound, &resp)
|
||||
s.DoJSON("POST", "/api/latest/fleet/hosts/1/software/1/uninstall", nil, http.StatusNotFound, &resp)
|
||||
|
||||
// create a host that doesn't have fleetd installed
|
||||
h, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
|
|
@ -10947,29 +10958,65 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
|
||||
// request fails
|
||||
resp = installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/1", h.ID), nil, http.StatusUnprocessableEntity, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/1/install", h.ID), nil, http.StatusUnprocessableEntity, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/1/uninstall", h.ID), nil, http.StatusUnprocessableEntity, &resp)
|
||||
|
||||
// host installs fleetd
|
||||
setOrbitEnrollment(t, h, s.ds)
|
||||
orbitKey := setOrbitEnrollment(t, h, s.ds)
|
||||
h.OrbitNodeKey = &orbitKey
|
||||
|
||||
// request fails because of non-existent title
|
||||
resp = installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/1", h.ID), nil, http.StatusBadRequest, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/1/install", h.ID), nil, http.StatusBadRequest, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/1/uninstall", h.ID), nil, http.StatusBadRequest, &resp)
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "another install script",
|
||||
PreInstallQuery: "another pre install query",
|
||||
PostInstallScript: "another post install script",
|
||||
UninstallScript: "another uninstall script with $PACKAGE_ID",
|
||||
Filename: "ruby.deb",
|
||||
Title: "ruby",
|
||||
TeamID: teamID,
|
||||
}
|
||||
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
|
||||
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
|
||||
|
||||
// Get title with software installer
|
||||
respTitle := getSoftwareTitleResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &respTitle, "team_id",
|
||||
fmt.Sprintf("%d", *teamID))
|
||||
require.NotNil(t, respTitle.SoftwareTitle.SoftwarePackage)
|
||||
assert.Equal(t, "another install script", respTitle.SoftwareTitle.SoftwarePackage.InstallScript)
|
||||
assert.Equal(t, `another uninstall script with "ruby"`, respTitle.SoftwareTitle.SoftwarePackage.UninstallScript)
|
||||
|
||||
// Upload another package for another platform
|
||||
payloadDummy := &fleet.UploadSoftwareInstallerPayload{
|
||||
Filename: "dummy_installer.pkg",
|
||||
Title: "DummyApp.app",
|
||||
TeamID: teamID,
|
||||
}
|
||||
s.uploadSoftwareInstaller(payloadDummy, http.StatusOK, "")
|
||||
pkgTitleID := getSoftwareTitleID(t, s.ds, payloadDummy.Title, "apps")
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", pkgTitleID), nil, http.StatusOK, &respTitle, "team_id",
|
||||
fmt.Sprintf("%d", *teamID))
|
||||
require.NotNil(t, respTitle.SoftwareTitle.SoftwarePackage)
|
||||
assert.NotEmpty(t, respTitle.SoftwareTitle.SoftwarePackage.InstallScript)
|
||||
assert.NotEmpty(t, respTitle.SoftwareTitle.SoftwarePackage.UninstallScript)
|
||||
assert.NotContains(t, respTitle.SoftwareTitle.SoftwarePackage.UninstallScript, "$PACKAGE_ID")
|
||||
assert.Contains(t, respTitle.SoftwareTitle.SoftwarePackage.UninstallScript, "com.example.dummy")
|
||||
|
||||
// install/uninstall request fails for the wrong platform
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h.ID, pkgTitleID), nil, http.StatusBadRequest, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", h.ID, pkgTitleID), nil, http.StatusBadRequest, &resp)
|
||||
|
||||
// delete software installer which we will not use
|
||||
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/software/titles/%d/available_for_install", pkgTitleID), nil, http.StatusNoContent,
|
||||
"team_id", fmt.Sprintf("%d", *teamID))
|
||||
|
||||
// install script request succeeds
|
||||
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "deb_packages")
|
||||
resp = installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
|
||||
// Get the results, should be pending
|
||||
getHostSoftwareResp := getHostSoftwareResponse{}
|
||||
|
|
@ -10978,16 +11025,21 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage)
|
||||
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall)
|
||||
require.NotNil(t, getHostSoftwareResp.Software[0].Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *getHostSoftwareResp.Software[0].Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *getHostSoftwareResp.Software[0].Status)
|
||||
assert.Nil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastUninstall)
|
||||
installUUID := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
|
||||
|
||||
gsirr := getSoftwareInstallResultsResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &gsirr)
|
||||
require.NoError(t, gsirr.Err)
|
||||
require.NotNil(t, gsirr.Results)
|
||||
results := gsirr.Results
|
||||
require.Equal(t, installUUID, results.InstallUUID)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, results.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, results.Status)
|
||||
|
||||
// Can't install/uninstall if software install is pending
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h.ID, titleID), nil, http.StatusBadRequest, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", h.ID, titleID), nil, http.StatusBadRequest, &resp)
|
||||
|
||||
// create 3 more hosts, will have statuses installed, failed and one with two
|
||||
// install requests - one failed and the latest install pending
|
||||
|
|
@ -10997,7 +11049,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
err = s.ds.AddHostsToTeam(context.Background(), teamID, []uint{h2.ID, h3.ID, h4.ID})
|
||||
require.NoError(t, err)
|
||||
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h2.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h2.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h2.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
installUUID2 := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
|
||||
|
|
@ -11009,7 +11061,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
"install_script_output": "ok"
|
||||
}`, *h2.OrbitNodeKey, installUUID2)), http.StatusNoContent)
|
||||
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h3.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h3.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h3.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
installUUID3 := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
|
||||
|
|
@ -11021,7 +11073,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
"install_script_output": "failed"
|
||||
}`, *h3.OrbitNodeKey, installUUID3)), http.StatusNoContent)
|
||||
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h4.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h4.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h4.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
installUUID4a := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
|
||||
|
|
@ -11031,7 +11083,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
"pre_install_condition_output": ""
|
||||
}`, *h4.OrbitNodeKey, installUUID4a)), http.StatusNoContent)
|
||||
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", h4.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h4.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h4.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
installUUID4b := getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall.InstallUUID
|
||||
|
|
@ -11047,9 +11099,9 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
require.Equal(t, "ruby.deb", titleResp.SoftwareTitle.SoftwarePackage.Name)
|
||||
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerStatusSummary{
|
||||
Installed: 1,
|
||||
Pending: 2,
|
||||
Failed: 1,
|
||||
Installed: 1,
|
||||
PendingInstall: 2,
|
||||
FailedInstall: 1,
|
||||
}, *titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
|
||||
// status is reflected in list hosts responses and counts when filtering by software title and status
|
||||
|
|
@ -11159,7 +11211,101 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
assert.Nil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall)
|
||||
assert.Empty(t, getHostSoftwareResp.Software[0].InstalledVersions, "Installed versions should now not exist")
|
||||
|
||||
// Access software install result after host is deleted
|
||||
// Mark original install successful
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
"install_uuid": %q,
|
||||
"pre_install_condition_output": "ok",
|
||||
"install_script_exit_code": 0,
|
||||
"install_script_output": "ok"
|
||||
}`, *h.OrbitNodeKey, installUUID)), http.StatusNoContent)
|
||||
|
||||
// Do uninstall
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", h.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
assert.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall)
|
||||
assert.Equal(t, fleet.SoftwareUninstallPending, *getHostSoftwareResp.Software[0].Status)
|
||||
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastUninstall)
|
||||
uninstallExecutionID := getHostSoftwareResp.Software[0].SoftwarePackage.LastUninstall.ExecutionID
|
||||
|
||||
// Uninstall should show up as a pending activity
|
||||
var listUpcomingAct listHostUpcomingActivitiesResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", h.ID), nil, http.StatusOK, &listUpcomingAct)
|
||||
require.Len(t, listUpcomingAct.Activities, 1)
|
||||
assert.Equal(t, fleet.ActivityTypeUninstalledSoftware{}.ActivityName(), listUpcomingAct.Activities[0].Type)
|
||||
details := make(map[string]interface{}, 5)
|
||||
require.NoError(t, json.Unmarshal(*listUpcomingAct.Activities[0].Details, &details))
|
||||
assert.EqualValues(t, fleet.SoftwareUninstallPending, details["status"])
|
||||
|
||||
// Check that status is reflected in software title response
|
||||
titleResp = getSoftwareTitleResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id",
|
||||
strconv.Itoa(int(*teamID)))
|
||||
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage)
|
||||
assert.Equal(t, "ruby.deb", titleResp.SoftwareTitle.SoftwarePackage.Name)
|
||||
require.NotNil(t, titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
assert.Equal(t, fleet.SoftwareInstallerStatusSummary{
|
||||
PendingInstall: 1,
|
||||
FailedInstall: 1,
|
||||
PendingUninstall: 1,
|
||||
}, *titleResp.SoftwareTitle.SoftwarePackage.Status)
|
||||
|
||||
// Another install/uninstall cannot be send once an uninstall is pending
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", h.ID, titleID), nil, http.StatusBadRequest, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", h.ID, titleID), nil, http.StatusBadRequest, &resp)
|
||||
|
||||
// Host sends successful uninstall result
|
||||
var orbitPostScriptResp orbitPostScriptResultResponse
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *h.OrbitNodeKey,
|
||||
uninstallExecutionID)),
|
||||
http.StatusOK, &orbitPostScriptResp)
|
||||
|
||||
// Check activity feed
|
||||
var activitiesResp listActivitiesResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", h.ID), nil, http.StatusOK, &activitiesResp, "order_key", "a.id",
|
||||
"order_direction", "desc")
|
||||
require.NotEmpty(t, activitiesResp.Activities)
|
||||
assert.Equal(t, fleet.ActivityTypeUninstalledSoftware{}.ActivityName(), activitiesResp.Activities[0].Type)
|
||||
details = make(map[string]interface{}, 5)
|
||||
require.NoError(t, json.Unmarshal(*activitiesResp.Activities[0].Details, &details))
|
||||
assert.Equal(t, "uninstalled", details["status"])
|
||||
|
||||
// Software should be available for install again
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
assert.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall)
|
||||
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastUninstall)
|
||||
assert.Nil(t, getHostSoftwareResp.Software[0].Status)
|
||||
|
||||
// Uninstall again, but this time with a failed result
|
||||
beforeUninstall := time.Now()
|
||||
// Since host_script_results does not use fine-grained timestamps yet, we adjust
|
||||
beforeUninstall = beforeUninstall.Add(-time.Second)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", h.ID, titleID), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSoftwareResp)
|
||||
require.Len(t, getHostSoftwareResp.Software, 1)
|
||||
assert.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastInstall)
|
||||
assert.Equal(t, fleet.SoftwareUninstallPending, *getHostSoftwareResp.Software[0].Status)
|
||||
require.NotNil(t, getHostSoftwareResp.Software[0].SoftwarePackage.LastUninstall)
|
||||
uninstallExecutionID = getHostSoftwareResp.Software[0].SoftwarePackage.LastUninstall.ExecutionID
|
||||
// Host sends failed uninstall result
|
||||
s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
|
||||
json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 1, "output": "not ok"}`, *h.OrbitNodeKey,
|
||||
uninstallExecutionID)),
|
||||
http.StatusOK, &orbitPostScriptResp)
|
||||
|
||||
// Check activity feed
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities", h.ID), nil, http.StatusOK, &activitiesResp, "order_key", "a.id",
|
||||
"order_direction", "desc")
|
||||
require.NotEmpty(t, activitiesResp.Activities)
|
||||
assert.Equal(t, fleet.ActivityTypeUninstalledSoftware{}.ActivityName(), activitiesResp.Activities[0].Type)
|
||||
details = make(map[string]interface{}, 5)
|
||||
require.NoError(t, json.Unmarshal(*activitiesResp.Activities[0].Details, &details))
|
||||
assert.Equal(t, "failed", details["status"])
|
||||
|
||||
// Access software install/uninstall result after host is deleted
|
||||
err = s.ds.DeleteHost(context.Background(), h.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
|
|
@ -11168,12 +11314,21 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
|
|||
require.NotNil(t, instResult.HostDeletedAt)
|
||||
|
||||
gsirr = getSoftwareInstallResultsResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/results/%s", installUUID), nil, http.StatusOK, &gsirr)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &gsirr)
|
||||
require.NoError(t, gsirr.Err)
|
||||
require.NotNil(t, gsirr.Results)
|
||||
results = gsirr.Results
|
||||
require.Equal(t, installUUID, results.InstallUUID)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, results.Status)
|
||||
require.Equal(t, fleet.SoftwareInstalled, results.Status)
|
||||
|
||||
var scriptResultResp getScriptResultResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/scripts/results/"+uninstallExecutionID, nil, http.StatusOK, &scriptResultResp)
|
||||
assert.Equal(t, h.ID, scriptResultResp.HostID)
|
||||
assert.NotEmpty(t, scriptResultResp.ScriptContents)
|
||||
require.NotNil(t, scriptResultResp.ExitCode)
|
||||
assert.EqualValues(t, 1, *scriptResultResp.ExitCode)
|
||||
assert.Equal(t, "not ok", scriptResultResp.Output)
|
||||
assert.Less(t, beforeUninstall, scriptResultResp.CreatedAt)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestSelfServiceSoftwareInstall() {
|
||||
|
|
@ -11225,7 +11380,7 @@ func (s *integrationEnterpriseTestSuite) TestSelfServiceSoftwareInstall() {
|
|||
require.Equal(t, host1.ID, details.HostID)
|
||||
require.Equal(t, details.SoftwareTitle, payloadSS.Title)
|
||||
require.True(t, details.SelfService)
|
||||
require.EqualValues(t, fleet.SoftwareInstallerPending, details.Status)
|
||||
require.EqualValues(t, fleet.SoftwareInstallPending, details.Status)
|
||||
installID := details.InstallUUID
|
||||
|
||||
// record the installation results
|
||||
|
|
@ -11255,7 +11410,7 @@ func (s *integrationEnterpriseTestSuite) TestSelfServiceSoftwareInstall() {
|
|||
require.Equal(t, host1.ID, details.HostID)
|
||||
require.Equal(t, details.SoftwareTitle, payloadSS.Title)
|
||||
require.True(t, details.SelfService)
|
||||
require.EqualValues(t, fleet.SoftwareInstallerInstalled, details.Status)
|
||||
require.EqualValues(t, fleet.SoftwareInstalled, details.Status)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
||||
|
|
@ -11306,7 +11461,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
titleIDs := []uint{titleID, titleID2, titleID3}
|
||||
for i := 0; i < len(installUUIDs); i++ {
|
||||
resp := installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/install/%d", host.ID, titleIDs[i]), nil, http.StatusAccepted, &resp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/%d/install", host.ID, titleIDs[i]), nil, http.StatusAccepted, &resp)
|
||||
installUUIDs[i] = latestInstallUUID()
|
||||
}
|
||||
|
||||
|
|
@ -11320,7 +11475,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
}
|
||||
checkResults := func(want result) {
|
||||
var resp getSoftwareInstallResultsResponse
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/install/results/"+want.InstallUUID, nil, http.StatusOK, &resp)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/install/%s/results", want.InstallUUID), nil, http.StatusOK, &resp)
|
||||
|
||||
assert.Equal(t, want.HostID, resp.Results.HostID)
|
||||
assert.Equal(t, want.InstallUUID, resp.Results.InstallUUID)
|
||||
|
|
@ -11342,7 +11497,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
checkResults(result{
|
||||
HostID: host.ID,
|
||||
InstallUUID: installUUIDs[0],
|
||||
Status: fleet.SoftwareInstallerFailed,
|
||||
Status: fleet.SoftwareInstallFailed,
|
||||
PreInstallQueryOutput: ptr.String(fleet.SoftwareInstallerQuerySuccessCopy),
|
||||
Output: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerInstallFailCopy, "failed")),
|
||||
})
|
||||
|
|
@ -11352,7 +11507,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
SoftwareTitle: payload.Title,
|
||||
SoftwarePackage: payload.Filename,
|
||||
InstallUUID: installUUIDs[0],
|
||||
Status: string(fleet.SoftwareInstallerFailed),
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
}
|
||||
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
|
|
@ -11366,7 +11521,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
checkResults(result{
|
||||
HostID: host.ID,
|
||||
InstallUUID: installUUIDs[1],
|
||||
Status: fleet.SoftwareInstallerFailed,
|
||||
Status: fleet.SoftwareInstallFailed,
|
||||
PreInstallQueryOutput: ptr.String(fleet.SoftwareInstallerQueryFailCopy),
|
||||
})
|
||||
wantAct = fleet.ActivityTypeInstalledSoftware{
|
||||
|
|
@ -11375,7 +11530,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
SoftwareTitle: payload2.Title,
|
||||
SoftwarePackage: payload2.Filename,
|
||||
InstallUUID: installUUIDs[1],
|
||||
Status: string(fleet.SoftwareInstallerFailed),
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
}
|
||||
s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
|
|
@ -11393,7 +11548,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
checkResults(result{
|
||||
HostID: host.ID,
|
||||
InstallUUID: installUUIDs[2],
|
||||
Status: fleet.SoftwareInstallerInstalled,
|
||||
Status: fleet.SoftwareInstalled,
|
||||
PreInstallQueryOutput: ptr.String(fleet.SoftwareInstallerQuerySuccessCopy),
|
||||
Output: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerInstallSuccessCopy, "success")),
|
||||
PostInstallScriptOutput: ptr.String(fmt.Sprintf(fleet.SoftwareInstallerPostInstallSuccessCopy, "ok")),
|
||||
|
|
@ -11404,7 +11559,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() {
|
|||
SoftwareTitle: payload3.Title,
|
||||
SoftwarePackage: payload3.Filename,
|
||||
InstallUUID: installUUIDs[2],
|
||||
Status: string(fleet.SoftwareInstallerInstalled),
|
||||
Status: string(fleet.SoftwareInstalled),
|
||||
}
|
||||
lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
|
||||
|
|
@ -11559,6 +11714,7 @@ func (s *integrationEnterpriseTestSuite) uploadSoftwareInstaller(
|
|||
require.NoError(t, w.WriteField("install_script", payload.InstallScript))
|
||||
require.NoError(t, w.WriteField("pre_install_query", payload.PreInstallQuery))
|
||||
require.NoError(t, w.WriteField("post_install_script", payload.PostInstallScript))
|
||||
require.NoError(t, w.WriteField("uninstall_script", payload.UninstallScript))
|
||||
if payload.SelfService {
|
||||
require.NoError(t, w.WriteField("self_service", "true"))
|
||||
}
|
||||
|
|
@ -12900,7 +13056,8 @@ func (s *integrationEnterpriseTestSuite) TestVPPAppsWithoutMDM() {
|
|||
}, &team.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", orbitHost.ID, app.TitleID), &installSoftwareRequest{}, http.StatusUnprocessableEntity)
|
||||
r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", orbitHost.ID, app.TitleID), &installSoftwareRequest{},
|
||||
http.StatusUnprocessableEntity)
|
||||
require.Contains(t, extractServerErrorText(r.Body), "Couldn't install. MDM is turned off. Please make sure that MDM is turned on to install App Store apps.")
|
||||
}
|
||||
|
||||
|
|
@ -13258,12 +13415,12 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.NotEmpty(t, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status)
|
||||
prevExecutionID := host1LastInstall.ExecutionID
|
||||
|
||||
// Request a manual installation on the host for the same installer, which should fail.
|
||||
var installResp installSoftwareResponse
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d",
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install",
|
||||
host1Team1.ID, dummyInstallerPkgTitleID), nil, http.StatusBadRequest, &installResp)
|
||||
|
||||
// Submit same results as before, which should not trigger a installation because the policy is already failing.
|
||||
|
|
@ -13282,7 +13439,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, prevExecutionID, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status)
|
||||
|
||||
// Submit same results but policy1Team1 now passes,
|
||||
// and then submit again but policy1Team1 fails.
|
||||
|
|
@ -13311,7 +13468,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
require.NotNil(t, host1LastInstall)
|
||||
require.Equal(t, prevExecutionID, host1LastInstall.ExecutionID)
|
||||
require.NotNil(t, host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host1LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host1LastInstall.Status)
|
||||
|
||||
// host2Team1 is failing policy2Team1 and policy3Team1 policies.
|
||||
distributedResp = submitDistributedQueryResultsResponse{}
|
||||
|
|
@ -13328,7 +13485,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
require.NotNil(t, host2LastInstall)
|
||||
require.NotEmpty(t, host2LastInstall.ExecutionID)
|
||||
require.NotNil(t, host2LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host2LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host2LastInstall.Status)
|
||||
|
||||
// Associate fleet-osquery.msi to policy4Team2.
|
||||
mtplr = modifyTeamPolicyResponse{}
|
||||
|
|
@ -13352,7 +13509,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
require.NotNil(t, host3LastInstall)
|
||||
require.NotEmpty(t, host3LastInstall.ExecutionID)
|
||||
require.NotNil(t, host3LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallerPending, *host3LastInstall.Status)
|
||||
require.Equal(t, fleet.SoftwareInstallPending, *host3LastInstall.Status)
|
||||
host3LastInstallDetails, err := s.ds.GetSoftwareInstallDetails(ctx, host3LastInstall.ExecutionID)
|
||||
require.NoError(t, err)
|
||||
// Even if fleet-osquery.msi was uploaded as Self-service, it was installed by Fleet, so
|
||||
|
|
@ -13454,8 +13611,8 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
"software_package": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "failed"
|
||||
}`, host2Team1.ID, host2Team1.DisplayName(), "ruby", "ruby.deb", host2LastInstall.ExecutionID), 0)
|
||||
"status": "%s"
|
||||
}`, host2Team1.ID, host2Team1.DisplayName(), "ruby", "ruby.deb", host2LastInstall.ExecutionID, fleet.SoftwareInstallFailed), 0)
|
||||
|
||||
// Check that the activity item generated for ruby.deb installation has a null user,
|
||||
// but has name and email set.
|
||||
|
|
@ -13491,8 +13648,9 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
|
|||
"software_package": "%s",
|
||||
"self_service": false,
|
||||
"install_uuid": "%s",
|
||||
"status": "failed"
|
||||
}`, host3Team2.ID, host3Team2.DisplayName(), "Fleet osquery", "fleet-osquery.msi", host3LastInstall.ExecutionID), 0)
|
||||
"status": "%s"
|
||||
}`, host3Team2.ID, host3Team2.DisplayName(), "Fleet osquery", "fleet-osquery.msi", host3LastInstall.ExecutionID,
|
||||
fleet.SoftwareInstallFailed), 0)
|
||||
|
||||
// Check that the activity item generated for fleet-osquery.msi installation has the admin user set as author.
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
|
|
|
|||
|
|
@ -10707,7 +10707,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
|
||||
// attempt to install a VPP app on the non-MDM enrolled host
|
||||
installResp := installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", orbitHost.ID, macOSTitleID), &installSoftwareRequest{},
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", orbitHost.ID, macOSTitleID), &installSoftwareRequest{},
|
||||
http.StatusBadRequest, &installResp)
|
||||
|
||||
// Disable all teams token
|
||||
|
|
@ -10738,7 +10738,8 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", validToken.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
|
||||
|
||||
// Attempt to install non-existent app
|
||||
r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, 99999), &installSoftwareRequest{}, http.StatusBadRequest)
|
||||
r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, 99999), &installSoftwareRequest{},
|
||||
http.StatusBadRequest)
|
||||
require.Contains(t, extractServerErrorText(r.Body), "Couldn't install software. Software title is not available for install. Please add software package or App Store app to install.")
|
||||
|
||||
// Add app 1 as self-service
|
||||
|
|
@ -10757,7 +10758,8 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
|
||||
// Trigger install to the host
|
||||
installResp = installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, errTitleID), &installSoftwareRequest{}, http.StatusAccepted, &installResp)
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, errTitleID), &installSoftwareRequest{},
|
||||
http.StatusAccepted, &installResp)
|
||||
|
||||
s.Do("DELETE", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d", vppRes.Token.ID), &deleteVPPTokenRequest{}, http.StatusNoContent)
|
||||
|
||||
|
|
@ -10802,14 +10804,14 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
errApp.Name,
|
||||
errApp.AdamID,
|
||||
failedCmdUUID,
|
||||
fleet.SoftwareInstallerFailed,
|
||||
fleet.SoftwareInstallFailed,
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
||||
// Trigger install to the host
|
||||
installResp = installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", mdmHost.ID, macOSTitleID), &installSoftwareRequest{},
|
||||
http.StatusAccepted, &installResp)
|
||||
countResp = countHostsResponse{}
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts/count", nil, http.StatusOK, &countResp, "software_status", "pending", "team_id",
|
||||
|
|
@ -10849,7 +10851,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
addedApp.Name,
|
||||
addedApp.AdamID,
|
||||
cmdUUID,
|
||||
fleet.SoftwareInstallerInstalled,
|
||||
fleet.SoftwareInstalled,
|
||||
),
|
||||
0,
|
||||
)
|
||||
|
|
@ -10868,12 +10870,12 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
require.Empty(t, got1.AppStoreApp.Name) // Name is only present for installer packages
|
||||
require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
|
||||
require.NotNil(t, got1.Status)
|
||||
require.Equal(t, *got1.Status, fleet.SoftwareInstallerInstalled)
|
||||
require.Equal(t, *got1.Status, fleet.SoftwareInstalled)
|
||||
require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, cmdUUID)
|
||||
require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)
|
||||
require.Equal(t, got2.Name, "App 2")
|
||||
require.NotNil(t, got2.Status)
|
||||
require.Equal(t, *got2.Status, fleet.SoftwareInstallerFailed)
|
||||
require.Equal(t, *got2.Status, fleet.SoftwareInstallFailed)
|
||||
require.NotNil(t, got2.AppStoreApp)
|
||||
require.Equal(t, got2.AppStoreApp.AppStoreID, errApp.AdamID)
|
||||
require.Equal(t, got2.AppStoreApp.IconURL, ptr.String(errApp.IconURL))
|
||||
|
|
@ -10895,7 +10897,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
require.Empty(t, got1.AppStoreApp.Name)
|
||||
require.Equal(t, got1.AppStoreApp.Version, addedApp.LatestVersion)
|
||||
require.NotNil(t, got1.Status)
|
||||
require.Equal(t, *got1.Status, fleet.SoftwareInstallerInstalled)
|
||||
require.Equal(t, *got1.Status, fleet.SoftwareInstalled)
|
||||
require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, cmdUUID)
|
||||
require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)
|
||||
|
||||
|
|
@ -10988,7 +10990,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
&fleetSelfServiceSoftwareInstallRequest{}, http.StatusAccepted, &ssInstallResp)
|
||||
} else {
|
||||
installResp = installSoftwareResponse{}
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", installHost.ID, titleID),
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", installHost.ID, titleID),
|
||||
&installSoftwareRequest{}, http.StatusAccepted, &installResp)
|
||||
}
|
||||
countResp = countHostsResponse{}
|
||||
|
|
@ -11026,7 +11028,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
app.Name,
|
||||
app.AdamID,
|
||||
cmdUUID,
|
||||
fleet.SoftwareInstallerPending,
|
||||
fleet.SoftwareInstallPending,
|
||||
install.deviceToken != "",
|
||||
),
|
||||
string(*hostActivitiesResp.Activities[0].Details),
|
||||
|
|
@ -11054,7 +11056,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
app.Name,
|
||||
app.AdamID,
|
||||
cmdUUID,
|
||||
fleet.SoftwareInstallerInstalled,
|
||||
fleet.SoftwareInstalled,
|
||||
install.deviceToken != "",
|
||||
),
|
||||
0,
|
||||
|
|
@ -11074,7 +11076,7 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
|
|||
require.Equal(t, got1.AppStoreApp.IconURL, ptr.String(app.IconURL))
|
||||
require.Empty(t, got1.AppStoreApp.Name) // Name is only present for installer packages
|
||||
require.Equal(t, got1.AppStoreApp.Version, app.LatestVersion)
|
||||
require.Equal(t, *got1.Status, fleet.SoftwareInstallerInstalled)
|
||||
require.Equal(t, *got1.Status, fleet.SoftwareInstalled)
|
||||
require.Equal(t, got1.AppStoreApp.LastInstall.CommandUUID, cmdUUID)
|
||||
require.NotNil(t, got1.AppStoreApp.LastInstall.InstalledAt)
|
||||
foundInstalledApp = true
|
||||
|
|
|
|||
|
|
@ -685,7 +685,7 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
|
|||
|
||||
// always use the authenticated host's ID as host_id
|
||||
result.HostID = host.ID
|
||||
hsr, err := svc.ds.SetHostScriptExecutionResult(ctx, result)
|
||||
hsr, action, err := svc.ds.SetHostScriptExecutionResult(ctx, result)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "save host script result")
|
||||
}
|
||||
|
|
@ -707,20 +707,47 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host
|
|||
scriptName = scr.Name
|
||||
}
|
||||
|
||||
// TODO(sarah): We may need to special case lock/unlock script results here?
|
||||
if err := svc.NewActivity(
|
||||
ctx,
|
||||
user,
|
||||
fleet.ActivityTypeRanScript{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
ScriptExecutionID: hsr.ExecutionID,
|
||||
ScriptName: scriptName,
|
||||
Async: !hsr.SyncRequest,
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
||||
switch action {
|
||||
case "uninstall":
|
||||
// Get software title from execution ID
|
||||
softwareTitleName, err := svc.ds.GetSoftwareTitleNameFromExecutionID(ctx, hsr.ExecutionID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get software title from execution ID")
|
||||
}
|
||||
activityStatus := "failed"
|
||||
if hsr.ExitCode != nil && *hsr.ExitCode == 0 {
|
||||
activityStatus = "uninstalled"
|
||||
}
|
||||
if err := svc.NewActivity(
|
||||
ctx,
|
||||
user,
|
||||
fleet.ActivityTypeUninstalledSoftware{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
SoftwareTitle: softwareTitleName,
|
||||
ExecutionID: hsr.ExecutionID,
|
||||
Status: activityStatus,
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
||||
}
|
||||
default:
|
||||
// TODO(sarah): We may need to special case lock/unlock script results here?
|
||||
if err := svc.NewActivity(
|
||||
ctx,
|
||||
user,
|
||||
fleet.ActivityTypeRanScript{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
ScriptExecutionID: hsr.ExecutionID,
|
||||
ScriptName: scriptName,
|
||||
Async: !hsr.SyncRequest,
|
||||
},
|
||||
); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "create activity for script execution request")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -992,7 +1019,7 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f
|
|||
return ctxerr.Wrap(ctx, err, "save host software installation result")
|
||||
}
|
||||
|
||||
if status := result.Status(); status != fleet.SoftwareInstallerPending {
|
||||
if status := result.Status(); status != fleet.SoftwareInstallPending {
|
||||
hsi, err := svc.ds.GetSoftwareInstallResults(ctx, result.InstallUUID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "get host software installation result information")
|
||||
|
|
|
|||
|
|
@ -1713,7 +1713,7 @@ func (svc *Service) processSoftwareForNewlyFailingPolicies(
|
|||
}
|
||||
// hostLastInstall.Status == nil can happen when a software is installed by Fleet and later removed.
|
||||
if hostLastInstall != nil && hostLastInstall.Status != nil &&
|
||||
*hostLastInstall.Status == fleet.SoftwareInstallerPending {
|
||||
*hostLastInstall.Status == fleet.SoftwareInstallPending {
|
||||
// There's a pending install for this host and installer,
|
||||
// thus we do not queue another install request.
|
||||
level.Debug(svc.logger).Log(
|
||||
|
|
|
|||
|
|
@ -349,16 +349,17 @@ type getScriptResultRequest struct {
|
|||
}
|
||||
|
||||
type getScriptResultResponse struct {
|
||||
ScriptContents string `json:"script_contents"`
|
||||
ScriptID *uint `json:"script_id"`
|
||||
ExitCode *int64 `json:"exit_code"`
|
||||
Output string `json:"output"`
|
||||
Message string `json:"message"`
|
||||
HostName string `json:"hostname"`
|
||||
HostTimeout bool `json:"host_timeout"`
|
||||
HostID uint `json:"host_id"`
|
||||
ExecutionID string `json:"execution_id"`
|
||||
Runtime int `json:"runtime"`
|
||||
ScriptContents string `json:"script_contents"`
|
||||
ScriptID *uint `json:"script_id"`
|
||||
ExitCode *int64 `json:"exit_code"`
|
||||
Output string `json:"output"`
|
||||
Message string `json:"message"`
|
||||
HostName string `json:"hostname"`
|
||||
HostTimeout bool `json:"host_timeout"`
|
||||
HostID uint `json:"host_id"`
|
||||
ExecutionID string `json:"execution_id"`
|
||||
Runtime int `json:"runtime"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
|
@ -388,6 +389,7 @@ func getScriptResultEndpoint(ctx context.Context, request interface{}, svc fleet
|
|||
HostID: scriptResult.HostID,
|
||||
ExecutionID: scriptResult.ExecutionID,
|
||||
Runtime: scriptResult.Runtime,
|
||||
CreatedAt: scriptResult.CreatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type uploadSoftwareInstallerRequest struct {
|
|||
PreInstallQuery string
|
||||
PostInstallScript string
|
||||
SelfService bool
|
||||
UninstallScript string
|
||||
}
|
||||
|
||||
type uploadSoftwareInstallerResponse struct {
|
||||
|
|
@ -96,6 +97,11 @@ func (uploadSoftwareInstallerRequest) DecodeRequest(ctx context.Context, r *http
|
|||
decoded.InstallScript = val[0]
|
||||
}
|
||||
|
||||
val, ok = r.MultipartForm.Value["uninstall_script"]
|
||||
if ok && len(val) > 0 {
|
||||
decoded.UninstallScript = val[0]
|
||||
}
|
||||
|
||||
val, ok = r.MultipartForm.Value["pre_install_query"]
|
||||
if ok && len(val) > 0 {
|
||||
decoded.PreInstallQuery = val[0]
|
||||
|
|
@ -136,6 +142,7 @@ func uploadSoftwareInstallerEndpoint(ctx context.Context, request interface{}, s
|
|||
InstallerFile: ff,
|
||||
Filename: req.File.Filename,
|
||||
SelfService: req.SelfService,
|
||||
UninstallScript: req.UninstallScript,
|
||||
}
|
||||
|
||||
if err := svc.UploadSoftwareInstaller(ctx, payload); err != nil {
|
||||
|
|
@ -332,6 +339,28 @@ func (svc *Service) InstallSoftwareTitle(ctx context.Context, hostID uint, softw
|
|||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type uninstallSoftwareRequest struct {
|
||||
HostID uint `url:"host_id"`
|
||||
SoftwareTitleID uint `url:"software_title_id"`
|
||||
}
|
||||
|
||||
func uninstallSoftwareTitleEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*uninstallSoftwareRequest)
|
||||
|
||||
err := svc.UninstallSoftwareTitle(ctx, req.HostID, req.SoftwareTitleID)
|
||||
if err != nil {
|
||||
return installSoftwareResponse{Err: err}, nil
|
||||
}
|
||||
|
||||
return installSoftwareResponse{}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) UninstallSoftwareTitle(ctx context.Context, _ uint, _ uint) error {
|
||||
// skipauth: No authorization check needed due to implementation returning only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
return fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type getSoftwareInstallResultsRequest struct {
|
||||
InstallUUID string `url:"install_uuid"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue