Add support for install/uninstall script overrides, pre-install query, post-install script in FMA GitOps (#31803)
Also removed the automatic install flag on YAML FMAs as it's undocumented/unspec'd Fixes #25636. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/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) ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Allowed overriding install/uninstall scripts, and specifying pre-install queries and post-install scripts, for Fleet-maintained apps in GitOps
|
||||
@@ -1719,8 +1719,12 @@ func (svc *Service) softwareInstallerPayloadFromSlug(ctx context.Context, payloa
|
||||
if app.SHA256 != noCheckHash {
|
||||
payload.SHA256 = app.SHA256
|
||||
}
|
||||
payload.InstallScript = app.InstallScript
|
||||
payload.UninstallScript = app.UninstallScript
|
||||
if payload.InstallScript == "" {
|
||||
payload.InstallScript = app.InstallScript
|
||||
}
|
||||
if payload.UninstallScript == "" {
|
||||
payload.UninstallScript = app.UninstallScript
|
||||
}
|
||||
payload.FleetMaintained = true
|
||||
payload.MaintainedApp = app
|
||||
if len(payload.Categories) == 0 {
|
||||
@@ -2004,7 +2008,6 @@ func (svc *Service) softwareBatchUpload(
|
||||
}
|
||||
}
|
||||
extension := strings.TrimLeft(filepath.Ext(installer.Filename), ".")
|
||||
installer.AutomaticInstallQuery = p.MaintainedApp.AutomaticInstallQuery
|
||||
installer.Title = appName
|
||||
installer.Version = p.MaintainedApp.Version
|
||||
|
||||
@@ -2030,8 +2033,6 @@ func (svc *Service) softwareBatchUpload(
|
||||
installer.BundleIdentifier = p.MaintainedApp.BundleIdentifier()
|
||||
installer.StorageID = p.MaintainedApp.SHA256
|
||||
installer.FleetMaintainedAppID = &p.MaintainedApp.ID
|
||||
installer.AutomaticInstall = p.AutomaticInstall != nil && *p.AutomaticInstall
|
||||
installer.AutomaticInstallQuery = p.MaintainedApp.AutomaticInstallQuery
|
||||
}
|
||||
|
||||
var ext string
|
||||
|
||||
+28
-23
@@ -162,9 +162,9 @@ type SoftwarePackage struct {
|
||||
}
|
||||
|
||||
type Software struct {
|
||||
Packages []SoftwarePackage `json:"packages"`
|
||||
AppStoreApps []fleet.TeamSpecAppStoreApp `json:"app_store_apps"`
|
||||
FleetMaintainedApps []fleet.FleetMaintainedAppsSpec `json:"fleet_maintained_apps"`
|
||||
Packages []SoftwarePackage `json:"packages"`
|
||||
AppStoreApps []fleet.TeamSpecAppStoreApp `json:"app_store_apps"`
|
||||
FleetMaintainedApps []fleet.MaintainedAppSpec `json:"fleet_maintained_apps"`
|
||||
}
|
||||
|
||||
type GitOps struct {
|
||||
@@ -186,7 +186,7 @@ type GitOps struct {
|
||||
type GitOpsSoftware struct {
|
||||
Packages []*fleet.SoftwarePackageSpec
|
||||
AppStoreApps []*fleet.TeamSpecAppStoreApp
|
||||
FleetMaintainedApps []*fleet.FleetMaintainedAppsSpec
|
||||
FleetMaintainedApps []*fleet.MaintainedAppSpec
|
||||
}
|
||||
|
||||
type Logf func(format string, a ...interface{})
|
||||
@@ -1095,6 +1095,28 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
|
||||
continue
|
||||
}
|
||||
|
||||
item = item.ResolveSoftwarePackagePaths(baseDir)
|
||||
|
||||
// handle secrets
|
||||
if item.InstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, item.InstallScript.Path); err != nil {
|
||||
multiError = multierror.Append(multiError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if item.PostInstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, item.PostInstallScript.Path); err != nil {
|
||||
multiError = multierror.Append(multiError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if item.UninstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, item.UninstallScript.Path); err != nil {
|
||||
multiError = multierror.Append(multiError, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result.Software.FleetMaintainedApps = append(result.Software.FleetMaintainedApps, &item)
|
||||
}
|
||||
for _, item := range software.Packages {
|
||||
@@ -1117,9 +1139,9 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
|
||||
continue
|
||||
}
|
||||
|
||||
softwarePackageSpec = resolveSoftwarePackagePaths(filepath.Dir(softwarePackageSpec.ReferencedYamlPath), softwarePackageSpec)
|
||||
softwarePackageSpec = softwarePackageSpec.ResolveSoftwarePackagePaths(filepath.Dir(softwarePackageSpec.ReferencedYamlPath))
|
||||
} else {
|
||||
softwarePackageSpec = resolveSoftwarePackagePaths(baseDir, item.SoftwarePackageSpec)
|
||||
softwarePackageSpec = item.SoftwarePackageSpec.ResolveSoftwarePackagePaths(baseDir)
|
||||
}
|
||||
if softwarePackageSpec.InstallScript.Path != "" {
|
||||
if err := gatherFileSecrets(result, softwarePackageSpec.InstallScript.Path); err != nil {
|
||||
@@ -1194,23 +1216,6 @@ func gatherFileSecrets(result *GitOps, filePath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveSoftwarePackagePaths(baseDir string, softwareSpec fleet.SoftwarePackageSpec) fleet.SoftwarePackageSpec {
|
||||
if softwareSpec.PreInstallQuery.Path != "" {
|
||||
softwareSpec.PreInstallQuery.Path = resolveApplyRelativePath(baseDir, softwareSpec.PreInstallQuery.Path)
|
||||
}
|
||||
if softwareSpec.InstallScript.Path != "" {
|
||||
softwareSpec.InstallScript.Path = resolveApplyRelativePath(baseDir, softwareSpec.InstallScript.Path)
|
||||
}
|
||||
if softwareSpec.PostInstallScript.Path != "" {
|
||||
softwareSpec.PostInstallScript.Path = resolveApplyRelativePath(baseDir, softwareSpec.PostInstallScript.Path)
|
||||
}
|
||||
if softwareSpec.UninstallScript.Path != "" {
|
||||
softwareSpec.UninstallScript.Path = resolveApplyRelativePath(baseDir, softwareSpec.UninstallScript.Path)
|
||||
}
|
||||
|
||||
return softwareSpec
|
||||
}
|
||||
|
||||
func getDuplicateNames[T any](slice []T, getComparableString func(T) string) []string {
|
||||
// We are using the allKeys map as a set here. True means the item is a duplicate.
|
||||
allKeys := make(map[string]bool)
|
||||
|
||||
+19
-1
@@ -105,6 +105,9 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
"FLEET_SECRET_CLEMENTINE": "not-an-orange",
|
||||
"FLEET_SECRET_DURIAN": "fruity", // not used
|
||||
"FLEET_SECRET_EGGPLANT": "parmesan",
|
||||
},
|
||||
filePath: "testdata/team_config_no_paths.yml",
|
||||
isTeam: true,
|
||||
@@ -119,6 +122,9 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
"FLEET_SECRET_CLEMENTINE": "not-an-orange",
|
||||
"FLEET_SECRET_DURIAN": "fruity", // not used
|
||||
"FLEET_SECRET_EGGPLANT": "parmesan",
|
||||
},
|
||||
filePath: "testdata/team_config.yml",
|
||||
isTeam: true,
|
||||
@@ -133,6 +139,9 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
"FLEET_SECRET_NAME": "secret_name",
|
||||
"FLEET_SECRET_length": "10",
|
||||
"FLEET_SECRET_BANANA": "bread",
|
||||
"FLEET_SECRET_CLEMENTINE": "not-an-orange",
|
||||
"FLEET_SECRET_DURIAN": "fruity", // not used
|
||||
"FLEET_SECRET_EGGPLANT": "parmesan",
|
||||
},
|
||||
filePath: "testdata/team_config_only_sha256.yml",
|
||||
isTeam: true,
|
||||
@@ -188,6 +197,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
assert.Equal(t, "SampleSecret123", secrets.([]*fleet.EnrollSecret)[0].Secret)
|
||||
assert.Equal(t, "ABC", secrets.([]*fleet.EnrollSecret)[1].Secret)
|
||||
require.Len(t, gitops.Software.Packages, 2)
|
||||
require.Len(t, gitops.FleetSecrets, 6)
|
||||
for _, pkg := range gitops.Software.Packages {
|
||||
if strings.Contains(pkg.URL, "MicrosoftTeams") {
|
||||
assert.Equal(t, "testdata/lib/uninstall.sh", pkg.UninstallScript.Path)
|
||||
@@ -200,8 +210,16 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
switch fma.Slug {
|
||||
case "slack/darwin":
|
||||
require.ElementsMatch(t, fma.Categories, []string{"Productivity", "Communication"})
|
||||
require.Empty(t, fma.PreInstallQuery)
|
||||
require.Empty(t, fma.PostInstallScript)
|
||||
require.Empty(t, fma.InstallScript)
|
||||
require.Empty(t, fma.UninstallScript)
|
||||
case "box-drive/windows":
|
||||
require.ElementsMatch(t, fma.Categories, []string{"Productivity", "Developer tools"})
|
||||
require.NotEmpty(t, fma.PreInstallQuery)
|
||||
require.NotEmpty(t, fma.PostInstallScript)
|
||||
require.NotEmpty(t, fma.InstallScript)
|
||||
require.NotEmpty(t, fma.UninstallScript)
|
||||
default:
|
||||
assert.FailNow(t, "unexpected slug found in gitops file", "slug: %s", fma.Slug)
|
||||
}
|
||||
@@ -240,6 +258,7 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
activityExpiryWindow, ok := activityExpirySettings["activity_expiry_window"].(float64)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 30, int(activityExpiryWindow))
|
||||
require.Len(t, gitops.FleetSecrets, 4)
|
||||
|
||||
// Check labels
|
||||
require.Len(t, gitops.Labels, 2)
|
||||
@@ -274,7 +293,6 @@ func TestValidGitOpsYaml(t *testing.T) {
|
||||
assert.True(t, ok, "windows_migration_enabled not found")
|
||||
_, ok = gitops.Controls.WindowsUpdates.(map[string]interface{})
|
||||
assert.True(t, ok, "windows_updates not found")
|
||||
require.Len(t, gitops.FleetSecrets, 4)
|
||||
assert.Equal(t, "fleet_secret", gitops.FleetSecrets["FLEET_SECRET_FLEET_SECRET_"])
|
||||
assert.Equal(t, "secret_name", gitops.FleetSecrets["FLEET_SECRET_NAME"])
|
||||
assert.Equal(t, "10", gitops.FleetSecrets["FLEET_SECRET_length"])
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
echo $FLEET_SECRET_EGGPLANT
|
||||
+1
@@ -0,0 +1 @@
|
||||
echo $FLEET_SECRET_EGGPLANT
|
||||
+1
@@ -0,0 +1 @@
|
||||
- query: SELECT 1 FROM osquery_info
|
||||
+1
@@ -0,0 +1 @@
|
||||
echo $FLEET_SECRET_CLEMENTINE
|
||||
Vendored
+8
@@ -52,6 +52,14 @@ software:
|
||||
- Productivity
|
||||
- Communication
|
||||
- slug: box-drive/windows
|
||||
install_script:
|
||||
path: ./lib/install.sh
|
||||
uninstall_script:
|
||||
path: ./lib/uninstall-box.sh
|
||||
post_install_script:
|
||||
path: ./lib/post-install.sh
|
||||
pre_install_query:
|
||||
path: ./lib/preinstall-query.yml
|
||||
self_service: true
|
||||
categories:
|
||||
- Productivity
|
||||
|
||||
+8
@@ -154,6 +154,14 @@ software:
|
||||
- Productivity
|
||||
- Communication
|
||||
- slug: box-drive/windows
|
||||
install_script:
|
||||
path: ./lib/install.sh
|
||||
uninstall_script:
|
||||
path: ./lib/uninstall-box.sh
|
||||
post_install_script:
|
||||
path: ./lib/post-install.sh
|
||||
pre_install_query:
|
||||
path: ./lib/preinstall-query.yml
|
||||
self_service: true
|
||||
categories:
|
||||
- Productivity
|
||||
|
||||
@@ -56,3 +56,11 @@ software:
|
||||
categories:
|
||||
- Productivity
|
||||
- Developer tools
|
||||
install_script:
|
||||
path: ./lib/install.sh
|
||||
uninstall_script:
|
||||
path: ./lib/uninstall-box.sh
|
||||
post_install_script:
|
||||
path: ./lib/post-install.sh
|
||||
pre_install_query:
|
||||
path: ./lib/preinstall-query.yml
|
||||
|
||||
@@ -415,9 +415,8 @@ type SoftwareInstallerPayload struct {
|
||||
SHA256 string `json:"sha256"`
|
||||
Categories []string `json:"categories"`
|
||||
// This is to support FMAs
|
||||
Slug *string `json:"slug"`
|
||||
AutomaticInstall *bool `json:"automatic_install"`
|
||||
MaintainedApp *MaintainedApp `json:"-"`
|
||||
Slug *string `json:"slug"`
|
||||
MaintainedApp *MaintainedApp `json:"-"`
|
||||
}
|
||||
|
||||
type HostLockWipeStatus struct {
|
||||
|
||||
@@ -556,8 +556,7 @@ type SoftwarePackageSpec struct {
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
|
||||
// FMA
|
||||
Slug *string `json:"slug"`
|
||||
AutomaticInstall *bool `json:"automatic_install"`
|
||||
Slug *string `json:"slug"`
|
||||
|
||||
// ReferencedYamlPath is the resolved path of the file used to fill the
|
||||
// software package. Only present after parsing a GitOps file on the fleetctl
|
||||
@@ -572,19 +571,61 @@ type SoftwarePackageSpec struct {
|
||||
Categories []string `json:"categories"`
|
||||
}
|
||||
|
||||
type FleetMaintainedAppsSpec struct {
|
||||
Slug string `json:"slug"`
|
||||
AutomaticInstall *bool `json:"automatic_install"`
|
||||
SelfService bool `json:"self_service"`
|
||||
LabelsIncludeAny []string `json:"labels_include_any"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
Categories []string `json:"categories"`
|
||||
func (spec SoftwarePackageSpec) ResolveSoftwarePackagePaths(baseDir string) SoftwarePackageSpec {
|
||||
spec.PreInstallQuery.Path = resolveApplyRelativePath(baseDir, spec.PreInstallQuery.Path)
|
||||
spec.InstallScript.Path = resolveApplyRelativePath(baseDir, spec.InstallScript.Path)
|
||||
spec.PostInstallScript.Path = resolveApplyRelativePath(baseDir, spec.PostInstallScript.Path)
|
||||
spec.UninstallScript.Path = resolveApplyRelativePath(baseDir, spec.UninstallScript.Path)
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
func resolveApplyRelativePath(baseDir string, path string) string {
|
||||
if path != "" && baseDir != "" && !filepath.IsAbs(path) {
|
||||
return filepath.Join(baseDir, path)
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
type MaintainedAppSpec struct {
|
||||
Slug string `json:"slug"`
|
||||
SelfService bool `json:"self_service"`
|
||||
PreInstallQuery TeamSpecSoftwareAsset `json:"pre_install_query"`
|
||||
InstallScript TeamSpecSoftwareAsset `json:"install_script"`
|
||||
PostInstallScript TeamSpecSoftwareAsset `json:"post_install_script"`
|
||||
UninstallScript TeamSpecSoftwareAsset `json:"uninstall_script"`
|
||||
LabelsIncludeAny []string `json:"labels_include_any"`
|
||||
LabelsExcludeAny []string `json:"labels_exclude_any"`
|
||||
Categories []string `json:"categories"`
|
||||
}
|
||||
|
||||
func (spec MaintainedAppSpec) ToSoftwarePackageSpec() SoftwarePackageSpec {
|
||||
return SoftwarePackageSpec{
|
||||
Slug: &spec.Slug,
|
||||
PreInstallQuery: spec.PreInstallQuery,
|
||||
InstallScript: spec.InstallScript,
|
||||
PostInstallScript: spec.PostInstallScript,
|
||||
UninstallScript: spec.UninstallScript,
|
||||
SelfService: spec.SelfService,
|
||||
LabelsIncludeAny: spec.LabelsIncludeAny,
|
||||
LabelsExcludeAny: spec.LabelsExcludeAny,
|
||||
}
|
||||
}
|
||||
|
||||
func (spec MaintainedAppSpec) ResolveSoftwarePackagePaths(baseDir string) MaintainedAppSpec {
|
||||
spec.PreInstallQuery.Path = resolveApplyRelativePath(baseDir, spec.PreInstallQuery.Path)
|
||||
spec.InstallScript.Path = resolveApplyRelativePath(baseDir, spec.InstallScript.Path)
|
||||
spec.PostInstallScript.Path = resolveApplyRelativePath(baseDir, spec.PostInstallScript.Path)
|
||||
spec.UninstallScript.Path = resolveApplyRelativePath(baseDir, spec.UninstallScript.Path)
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
type SoftwareSpec struct {
|
||||
Packages optjson.Slice[SoftwarePackageSpec] `json:"packages,omitempty"`
|
||||
FleetMaintainedApps optjson.Slice[FleetMaintainedAppsSpec] `json:"fleet_maintained_apps,omitempty"`
|
||||
AppStoreApps optjson.Slice[TeamSpecAppStoreApp] `json:"app_store_apps,omitempty"`
|
||||
Packages optjson.Slice[SoftwarePackageSpec] `json:"packages,omitempty"`
|
||||
FleetMaintainedApps optjson.Slice[MaintainedAppSpec] `json:"fleet_maintained_apps,omitempty"`
|
||||
AppStoreApps optjson.Slice[TeamSpecAppStoreApp] `json:"app_store_apps,omitempty"`
|
||||
}
|
||||
|
||||
// HostSoftwareInstall represents installation of software on a host from a
|
||||
|
||||
@@ -1111,7 +1111,6 @@ func buildSoftwarePackagesPayload(specs []fleet.SoftwarePackageSpec, installDuri
|
||||
|
||||
if si.Slug != nil {
|
||||
softwarePayloads[i].Slug = si.Slug
|
||||
softwarePayloads[i].AutomaticInstall = si.AutomaticInstall
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1473,14 +1472,7 @@ func extractTmSpecsFleetMaintainedApps(tmSpecs []json.RawMessage) map[string][]f
|
||||
packages = []fleet.SoftwarePackageSpec{}
|
||||
} else {
|
||||
for _, app := range software.FleetMaintainedApps.Value {
|
||||
packages = append(packages, fleet.SoftwarePackageSpec{
|
||||
Slug: &app.Slug,
|
||||
AutomaticInstall: app.AutomaticInstall,
|
||||
SelfService: app.SelfService,
|
||||
LabelsIncludeAny: app.LabelsIncludeAny,
|
||||
LabelsExcludeAny: app.LabelsExcludeAny,
|
||||
Categories: app.Categories,
|
||||
})
|
||||
packages = append(packages, app.ToSoftwarePackageSpec())
|
||||
}
|
||||
}
|
||||
m[spec.Name] = packages
|
||||
@@ -2150,13 +2142,7 @@ func (c *Client) doGitOpsNoTeamSetupAndSoftware(
|
||||
}
|
||||
for _, software := range config.Software.FleetMaintainedApps {
|
||||
if software != nil {
|
||||
packages = append(packages, fleet.SoftwarePackageSpec{
|
||||
Slug: &software.Slug,
|
||||
AutomaticInstall: software.AutomaticInstall,
|
||||
SelfService: software.SelfService,
|
||||
LabelsIncludeAny: software.LabelsIncludeAny,
|
||||
LabelsExcludeAny: software.LabelsExcludeAny,
|
||||
})
|
||||
packages = append(packages, software.ToSoftwarePackageSpec())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12096,9 +12096,9 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// basic fleet maintained app
|
||||
// basic fleet maintained app with install script override
|
||||
softwareToInstall = []*fleet.SoftwareInstallerPayload{
|
||||
{Slug: &maintained1.Slug},
|
||||
{Slug: &maintained1.Slug, InstallScript: "echo 'Hello world'"},
|
||||
}
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, "", batchResponse.RequestUUID)
|
||||
@@ -12107,6 +12107,11 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
||||
require.NotNil(t, packages[0].URL)
|
||||
require.Nil(t, packages[0].TeamID)
|
||||
|
||||
var softwareTitleResponse getSoftwareTitleResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *packages[0].TitleID), nil, http.StatusOK, &softwareTitleResponse, "teamId", "0")
|
||||
require.Equal(t, "echo 'Hello world'", softwareTitleResponse.SoftwareTitle.SoftwarePackage.InstallScript)
|
||||
require.Contains(t, softwareTitleResponse.SoftwareTitle.SoftwarePackage.UninstallScript, "LOGGED_IN_USER") // should pass through FMA script
|
||||
|
||||
// with a team
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
|
||||
Reference in New Issue
Block a user