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:
Ian Littman
2025-08-13 07:48:36 -05:00
committed by GitHub
parent ccbdf6c02e
commit da9bac09eb
15 changed files with 146 additions and 62 deletions
+28 -23
View File
@@ -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
View File
@@ -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"])
+1
View File
@@ -0,0 +1 @@
echo $FLEET_SECRET_EGGPLANT
+1
View File
@@ -0,0 +1 @@
echo $FLEET_SECRET_EGGPLANT
+1
View File
@@ -0,0 +1 @@
- query: SELECT 1 FROM osquery_info
+1
View File
@@ -0,0 +1 @@
echo $FLEET_SECRET_CLEMENTINE
+8
View File
@@ -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
View File
@@ -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
+8
View File
@@ -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