diff --git a/cmd/maintained-apps/main.go b/cmd/maintained-apps/main.go index 28eb0f50db..c8b4a8482c 100644 --- a/cmd/maintained-apps/main.go +++ b/cmd/maintained-apps/main.go @@ -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") } diff --git a/ee/maintained-apps/README.md b/ee/maintained-apps/README.md index 28b6dbfe2d..a328287097 100644 --- a/ee/maintained-apps/README.md +++ b/ee/maintained-apps/README.md @@ -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. diff --git a/ee/maintained-apps/ingesters/homebrew/ingester.go b/ee/maintained-apps/ingesters/homebrew/ingester.go index 737a81ec03..3905495df4 100644 --- a/ee/maintained-apps/ingesters/homebrew/ingester.go +++ b/ee/maintained-apps/ingesters/homebrew/ingester.go @@ -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 { diff --git a/ee/maintained-apps/ingesters/winget/ingester.go b/ee/maintained-apps/ingesters/winget/ingester.go index 88a7abce8c..6044aa8157 100644 --- a/ee/maintained-apps/ingesters/winget/ingester.go +++ b/ee/maintained-apps/ingesters/winget/ingester.go @@ -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 { diff --git a/ee/maintained-apps/maintained_apps.go b/ee/maintained-apps/maintained_apps.go index 583cb936d0..bc52dd4ef4 100644 --- a/ee/maintained-apps/maintained_apps.go +++ b/ee/maintained-apps/maintained_apps.go @@ -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 {