Don't overwrite FMA outputs with latest manifest if input has "frozen" set to true (#30044)

Resolves #29218. No changes file as this is internal/FMA-related.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Ian Littman
2025-06-17 13:10:58 -05:00
committed by GitHub
parent 81a121c71d
commit 06a4c16907
5 changed files with 33 additions and 10 deletions
+19 -10
View File
@@ -13,6 +13,7 @@ import (
maintained_apps "github.com/fleetdm/fleet/v4/ee/maintained-apps"
"github.com/fleetdm/fleet/v4/ee/maintained-apps/ingesters/homebrew"
"github.com/fleetdm/fleet/v4/ee/maintained-apps/ingesters/winget"
"github.com/fleetdm/fleet/v4/pkg/file"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
@@ -38,8 +39,8 @@ func main() {
"ee/maintained-apps/inputs/winget": winget.IngestApps,
}
for p, i := range ingesters {
apps, err := i(ctx, logger, p, *slugPtr)
for inputDir, ingest := range ingesters {
apps, err := ingest(ctx, logger, inputDir, *slugPtr)
if err != nil {
level.Error(logger).Log("msg", "failed to ingest apps", "error", err)
}
@@ -74,15 +75,23 @@ func processOutput(ctx context.Context, app *maintained_apps.FMAManifestApp) err
return ctxerr.Wrap(ctx, err, "marshaling output app manifest")
}
// Overwrite the file, since right now we're only caring about 1 version (latest). If we
// care about previous data, it will be in our Git history.
outPath := path.Join(maintained_apps.OutputPath, app.SlugAppName())
outDir := path.Join(maintained_apps.OutputPath, app.SlugAppName())
if err := os.MkdirAll(outPath, os.ModePerm); err != nil {
if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
return ctxerr.Wrap(ctx, err)
}
if err := os.WriteFile(path.Join(maintained_apps.OutputPath, fmt.Sprintf("%s.json", app.Slug)), outBytes, 0o644); err != nil {
return ctxerr.Wrap(ctx, err, "writing output json file")
outFilePath := path.Join(maintained_apps.OutputPath, fmt.Sprintf("%s.json", app.Slug))
outFileExists, err := file.Exists(outFilePath)
if err != nil {
return ctxerr.Wrap(ctx, err, "checking if output json file exists")
}
// Overwrite the file unless frozen, since right now we're only caring about 1 version (latest). If we
// care about previous data, it will be in our Git history.
if !app.Frozen || !outFileExists {
if err := os.WriteFile(outFilePath, outBytes, 0o644); err != nil {
return ctxerr.Wrap(ctx, err, "writing output json file")
}
}
return nil
@@ -90,13 +99,13 @@ func processOutput(ctx context.Context, app *maintained_apps.FMAManifestApp) err
func updateAppsListFile(ctx context.Context, outApp *maintained_apps.FMAManifestApp) error {
appListFilePath := path.Join(maintained_apps.OutputPath, "apps.json")
file, err := os.ReadFile(appListFilePath)
inputJson, err := os.ReadFile(appListFilePath)
if err != nil {
return ctxerr.Wrap(ctx, err, "reading output apps list file")
}
var outputAppsFile maintained_apps.FMAListFile
if err := json.Unmarshal(file, &outputAppsFile); err != nil {
if err := json.Unmarshal(inputJson, &outputAppsFile); err != nil {
return ctxerr.Wrap(ctx, err, "unmarshaling output apps list file")
}
+9
View File
@@ -1,5 +1,14 @@
# Fleet-maintained apps (FMA)
## Freezing an existing app
Add `"frozen": true` to the appropriate input JSON file to pause automated updates to the corresponding output manifest.
To aid in testing, manifests will still be generated for frozen inputs if the output file doesn't exist.
Apps should be frozen when updating the manifest would introduce regressions on ability to install/uninstall the app.
Frozen apps should have bugs filed, and fixes for those bugs should unfreeze the app and bump it to the latest version
as part of the fix PR.
## Adding a new app
1. Decide on a source for the app's metadata. We currently support homebrew as a source for macOS apps.
@@ -166,6 +166,7 @@ func (i *brewIngester) ingestOne(ctx context.Context, app inputApp) (*maintained
out.UninstallScriptRef = maintained_apps.GetScriptRef(out.UninstallScript)
out.InstallScriptRef = maintained_apps.GetScriptRef(out.InstallScript)
out.Frozen = app.Frozen
return out, nil
}
@@ -184,6 +185,7 @@ type inputApp struct {
PreUninstallScripts []string `json:"pre_uninstall_scripts"`
PostUninstallScripts []string `json:"post_uninstall_scripts"`
DefaultCategories []string `json:"default_categories"`
Frozen bool `json:"frozen"`
}
type brewCask struct {
@@ -299,6 +299,7 @@ func (i *wingetIngester) ingestOne(ctx context.Context, input inputApp) (*mainta
out.UninstallScript = preProcessUninstallScript(uninstallScript, productCode)
out.InstallScriptRef = maintained_apps.GetScriptRef(out.InstallScript)
out.UninstallScriptRef = maintained_apps.GetScriptRef(out.UninstallScript)
out.Frozen = input.Frozen
return &out, nil
}
@@ -354,6 +355,7 @@ type inputApp struct {
// Whether to use "no_check" instead of the app's hash (e.g. for non-pinned download URLs)
IgnoreHash bool `json:"ignore_hash"`
DefaultCategories []string `json:"default_categories"`
Frozen bool `json:"frozen"`
}
type installerManifest struct {
+1
View File
@@ -34,6 +34,7 @@ type FMAManifestApp struct {
Slug string `json:"-"`
Name string `json:"-"`
DefaultCategories []string `json:"default_categories"`
Frozen bool `json:"-"`
}
func (a *FMAManifestApp) Platform() string {