mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #40054 # Checklist for submitter - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. - present in a previous PR ## Testing - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Updated logging infrastructure throughout the application to use Go's standard library logging system. This replaces the previous logging implementation with an improved, standardized approach. Logging output format may appear slightly different, but all diagnostic and operational logging capabilities remain fully intact and functional. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
198 lines
5.7 KiB
Go
198 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path"
|
|
"slices"
|
|
"strings"
|
|
|
|
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"
|
|
)
|
|
|
|
func main() {
|
|
slugPtr := flag.String("slug", "", "app slug")
|
|
debugPtr := flag.Bool("debug", false, "enable debug logging")
|
|
flag.Parse()
|
|
ctx := context.Background()
|
|
logLevel := slog.LevelInfo
|
|
if *debugPtr {
|
|
logLevel = slog.LevelDebug
|
|
}
|
|
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel}))
|
|
|
|
logger.InfoContext(ctx, "starting maintained app ingestion")
|
|
|
|
ingesters := map[string]maintained_apps.Ingester{
|
|
"ee/maintained-apps/inputs/homebrew": homebrew.IngestApps,
|
|
"ee/maintained-apps/inputs/winget": winget.IngestApps,
|
|
}
|
|
|
|
for inputDir, ingest := range ingesters {
|
|
apps, err := ingest(ctx, logger, inputDir, *slugPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
for _, app := range apps {
|
|
|
|
if app.IsEmpty() {
|
|
logger.InfoContext(ctx, "skipping manifest update due to empty output", "slug", app.Slug)
|
|
continue
|
|
}
|
|
|
|
if err := processOutput(ctx, app); err != nil {
|
|
logger.ErrorContext(ctx, "failed to process maintained app output", "err", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func processOutput(ctx context.Context, app *maintained_apps.FMAManifestApp) error {
|
|
// validate categories before writing any files
|
|
if err := validateCategories(ctx, app); err != nil {
|
|
// Make the validation failure very obvious on stderr.
|
|
fmt.Fprintf(
|
|
os.Stderr,
|
|
"maintained-apps: fatal error processing %s: %v\n",
|
|
app.Slug,
|
|
err,
|
|
)
|
|
// Wrap so callers still see a proper error.
|
|
return ctxerr.Wrap(ctx, err, "validating categories")
|
|
}
|
|
|
|
if err := updateAppsListFile(ctx, app); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "updating apps list file")
|
|
}
|
|
app.UniqueIdentifier = "" // make sure we don't leak unique_identifier into individual app manifests
|
|
|
|
outFile := maintained_apps.FMAManifestFile{
|
|
Versions: []*maintained_apps.FMAManifestApp{app},
|
|
Refs: map[string]string{app.UninstallScriptRef: app.UninstallScript, app.InstallScriptRef: app.InstallScript},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
encoder := json.NewEncoder(&buf)
|
|
encoder.SetEscapeHTML(false)
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(outFile); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "marshaling output app manifest")
|
|
}
|
|
outBytes := buf.Bytes()
|
|
|
|
outDir := path.Join(maintained_apps.OutputPath, app.SlugAppName())
|
|
|
|
if err := os.MkdirAll(outDir, os.ModePerm); err != nil {
|
|
return ctxerr.Wrap(ctx, err)
|
|
}
|
|
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
|
|
}
|
|
|
|
// Match types in frontend/interfaces/software.ts
|
|
var allowedCategories = map[string]struct{}{
|
|
"Browsers": {},
|
|
"Communication": {},
|
|
"Developer tools": {},
|
|
"Productivity": {},
|
|
"Security": {},
|
|
"Utilities": {},
|
|
}
|
|
|
|
func allowedCategoriesString() string {
|
|
cats := make([]string, 0, len(allowedCategories))
|
|
for c := range allowedCategories {
|
|
cats = append(cats, c)
|
|
}
|
|
slices.Sort(cats)
|
|
return strings.Join(cats, ", ")
|
|
}
|
|
|
|
// validateCategories ensures every category on the app is one of the supported values.
|
|
func validateCategories(ctx context.Context, app *maintained_apps.FMAManifestApp) error {
|
|
for _, c := range app.DefaultCategories {
|
|
if _, ok := allowedCategories[c]; !ok {
|
|
return ctxerr.New(ctx, fmt.Sprintf(
|
|
"invalid category %q for slug %s (allowed: %s)",
|
|
c, app.Slug, allowedCategoriesString(),
|
|
))
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func updateAppsListFile(ctx context.Context, outApp *maintained_apps.FMAManifestApp) error {
|
|
appListFilePath := path.Join(maintained_apps.OutputPath, "apps.json")
|
|
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(inputJson, &outputAppsFile); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "unmarshaling output apps list file")
|
|
}
|
|
|
|
var found bool
|
|
for _, a := range outputAppsFile.Apps {
|
|
if a.Slug == outApp.Slug {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
platform := outApp.Platform()
|
|
if platform == "" {
|
|
return ctxerr.New(ctx, fmt.Sprintf("invalid platform found for slug %s", outApp.Slug))
|
|
}
|
|
|
|
outputAppsFile.Apps = append(outputAppsFile.Apps, maintained_apps.FMAListFileApp{
|
|
Name: outApp.Name,
|
|
Slug: outApp.Slug,
|
|
Platform: platform,
|
|
UniqueIdentifier: outApp.UniqueIdentifier,
|
|
})
|
|
|
|
// Keep existing order
|
|
slices.SortFunc(outputAppsFile.Apps, func(a, b maintained_apps.FMAListFileApp) int { return strings.Compare(a.Slug, b.Slug) })
|
|
|
|
var buf bytes.Buffer
|
|
encoder := json.NewEncoder(&buf)
|
|
encoder.SetEscapeHTML(false)
|
|
encoder.SetIndent("", " ")
|
|
if err := encoder.Encode(outputAppsFile); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "marshaling updated output apps file")
|
|
}
|
|
updatedFile := buf.Bytes()
|
|
|
|
if err := os.WriteFile(appListFilePath, updatedFile, 0o644); err != nil {
|
|
return ctxerr.Wrap(ctx, err, "writing updated output apps file")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|