From 8509b18c46766b5cbee6704c58b60c0bfae252ae Mon Sep 17 00:00:00 2001 From: Ian Littman Date: Thu, 9 Apr 2026 17:36:18 -0500 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20Add=20fallback=20for=20FMA=20man?= =?UTF-8?q?ifest=20URL=20pulls=20(#43312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #42754 # 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), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually ## Summary by CodeRabbit * **Bug Fixes** * Improved app manifest retrieval with automatic fallback to hosted copies when the primary source is unavailable, reducing sync failures. * **Documentation** * Clarified that Fleet will fall back to hosted manifest copies if the new manifest site is inaccessible. * **New Features** * Streamlined maintained-app synchronization to use a simpler sync entrypoint and unified primary/fallback fetch logic. * **Tests** * Added comprehensive tests for primary/fallback fetch flows, error handling, large-response truncation, and environment-based overrides. --- changes/42751-r2-fma | 2 +- cmd/fleet/cron.go | 2 +- server/mdm/maintainedapps/sync.go | 144 +++++----- server/mdm/maintainedapps/sync_test.go | 305 +++++++++++++++++++++ server/mdm/maintainedapps/testing_utils.go | 11 +- server/service/testing_utils.go | 26 +- 6 files changed, 413 insertions(+), 77 deletions(-) create mode 100644 server/mdm/maintainedapps/sync_test.go diff --git a/changes/42751-r2-fma b/changes/42751-r2-fma index 6d9c719006..139b0944fa 100644 --- a/changes/42751-r2-fma +++ b/changes/42751-r2-fma @@ -1 +1 @@ -* Switched Fleet-maintained apps serving location from GitHub to https://maintained-apps.fleetdm.com/manifests. If you limit outbound Fleet server traffic, make sure it can access the new FMA manifests location. +* Switched Fleet-maintained apps serving location from GitHub to https://maintained-apps.fleetdm.com/manifests. If this site is inaccessible, Fleet will fall back to the previous GitHub-hosted copies of manifest files. diff --git a/cmd/fleet/cron.go b/cmd/fleet/cron.go index d9d31d95dc..c76a37c1da 100644 --- a/cmd/fleet/cron.go +++ b/cmd/fleet/cron.go @@ -2005,7 +2005,7 @@ func newMaintainedAppSchedule( // ensures it runs a few seconds after Fleet is started schedule.WithDefaultPrevRunCreatedAt(time.Now().Add(priorJobDiff)), schedule.WithJob("refresh_maintained_apps", func(ctx context.Context) error { - return maintained_apps.Refresh(ctx, ds, logger) + return maintained_apps.SyncAppsList(ctx, ds) }), ) diff --git a/server/mdm/maintainedapps/sync.go b/server/mdm/maintainedapps/sync.go index af6b91e376..6f46b37fa0 100644 --- a/server/mdm/maintainedapps/sync.go +++ b/server/mdm/maintainedapps/sync.go @@ -4,9 +4,9 @@ import ( "context" _ "embed" "encoding/json" + "errors" "fmt" "io" - "log/slog" "net/http" "time" @@ -30,10 +30,80 @@ type AppsList struct { } const fmaOutputsBase = "https://maintained-apps.fleetdm.com/manifests" +const fmaOutputsFallbackBase = "https://raw.githubusercontent.com/fleetdm/fleet/refs/heads/main/ee/maintained-apps/outputs" -// Refresh fetches the latest information about maintained apps from FMA's -// apps list on GitHub and updates the Fleet database with the new information. -func Refresh(ctx context.Context, ds fleet.Datastore, logger *slog.Logger) error { +// resolveBaseURLs returns the primary and fallback base URLs for FMA manifests, +// taking into account any dev-mode env var overrides. +func resolveBaseURLs() (primary, fallback string) { + primary = fmaOutputsBase + if baseFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_BASE_URL"); baseFromEnvVar != "" { + primary = baseFromEnvVar + } + + fallback = fmaOutputsFallbackBase + if fallbackFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL"); fallbackFromEnvVar != "" { + fallback = fallbackFromEnvVar + } + + return primary, fallback +} + +// fetchManifestFile fetches a manifest file from the primary FMA CDN, falling back to the +// fallback CDN if the primary fails. +func fetchManifestFile(ctx context.Context, path string) ([]byte, error) { + primaryBase, fallbackBase := resolveBaseURLs() + + body, primaryErr := doFetch(ctx, primaryBase, path) + if primaryErr == nil { + return body, nil + } + + // Primary failed; try fallback. + body, fallbackErr := doFetch(ctx, fallbackBase, path) + if fallbackErr == nil { + return body, nil + } + + return nil, ctxerr.Errorf(ctx, "fetching FMA manifest file %q: primary (%s) failed: %v; fallback (%s) also failed: %v", + path, primaryBase, primaryErr, fallbackBase, fallbackErr) +} + +// doFetch performs a single HTTP GET for baseURL+path and returns the response +// body on success (HTTP 200). Any other outcome is returned as an error. +func doFetch(ctx context.Context, baseURL, path string) ([]byte, error) { + httpClient := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s%s", baseURL, path), nil) + if err != nil { + return nil, fmt.Errorf("create http request: %w", err) + } + + res, err := httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("execute http request: %w", err) + } + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("read http response body: %w", err) + } + + switch res.StatusCode { + case http.StatusOK: + return body, nil + case http.StatusNotFound: + return nil, errors.New("not found (HTTP 404)") + default: + if len(body) > 512 { + body = body[:512] + } + return nil, fmt.Errorf("HTTP status %d: %s", res.StatusCode, string(body)) + } +} + +// SyncAppsList fetches the latest FMA apps list and updates the apps list copy cached in the DB +func SyncAppsList(ctx context.Context, ds fleet.Datastore) error { appsList, err := FetchAppsList(ctx) if err != nil { return err @@ -43,38 +113,9 @@ func Refresh(ctx context.Context, ds fleet.Datastore, logger *slog.Logger) error } func FetchAppsList(ctx context.Context) (*AppsList, error) { - httpClient := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)) - baseURL := fmaOutputsBase - if baseFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_BASE_URL"); baseFromEnvVar != "" { - baseURL = baseFromEnvVar - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/apps.json", baseURL), nil) + body, err := fetchManifestFile(ctx, "/apps.json") if err != nil { - return nil, ctxerr.Wrap(ctx, err, "create http request") - } - - res, err := httpClient.Do(req) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "execute http request") - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "read http response body") - } - - switch res.StatusCode { - case http.StatusOK: - // success, go on - case http.StatusNotFound: - return nil, ctxerr.New(ctx, "maintained apps list not found") - default: - if len(body) > 512 { - body = body[:512] - } - return nil, ctxerr.Errorf(ctx, "apps list returned HTTP status %d: %s", res.StatusCode, string(body)) + return nil, ctxerr.Wrap(ctx, err, "fetch apps list") } var appsList AppsList @@ -163,38 +204,9 @@ func Hydrate(ctx context.Context, app *fleet.MaintainedApp, version string, team return app, nil } - httpClient := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)) - baseURL := fmaOutputsBase - if baseFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_BASE_URL"); baseFromEnvVar != "" { - baseURL = baseFromEnvVar - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s.json", baseURL, app.Slug), nil) + body, err := fetchManifestFile(ctx, fmt.Sprintf("/%s.json", app.Slug)) if err != nil { - return nil, ctxerr.Wrap(ctx, err, "create http request") - } - - res, err := httpClient.Do(req) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "execute http request") - } - defer res.Body.Close() - - body, err := io.ReadAll(res.Body) - if err != nil { - return nil, ctxerr.Wrap(ctx, err, "read http response body") - } - - switch res.StatusCode { - case http.StatusOK: - // success, go on - case http.StatusNotFound: - return nil, ctxerr.New(ctx, "app not found in Fleet manifests") - default: - if len(body) > 512 { - body = body[:512] - } - return nil, ctxerr.Errorf(ctx, "manifest retrieval returned HTTP status %d: %s", res.StatusCode, string(body)) + return nil, ctxerr.Wrap(ctx, err, "fetch app manifest") } var manifest ma.FMAManifestFile diff --git a/server/mdm/maintainedapps/sync_test.go b/server/mdm/maintainedapps/sync_test.go new file mode 100644 index 0000000000..af21237358 --- /dev/null +++ b/server/mdm/maintainedapps/sync_test.go @@ -0,0 +1,305 @@ +package maintained_apps + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/fleetdm/fleet/v4/server/dev_mode" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// newTestAppsJSON returns a valid apps.json payload for testing. +func newTestAppsJSON() []byte { + data, _ := json.Marshal(AppsList{ + Version: 2, + Apps: []appListing{ + {Name: "Test App", Slug: "test-app", Platform: "darwin", UniqueIdentifier: "com.test.app"}, + }, + }) + return data +} + +func TestFetchAppsListPrimarySuccess(t *testing.T) { + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/apps.json", r.URL.Path) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(newTestAppsJSON()) + })) + t.Cleanup(primary.Close) + + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Error("fallback should not be called when primary succeeds") + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + apps, err := FetchAppsList(t.Context()) + require.NoError(t, err) + require.Len(t, apps.Apps, 1) + assert.Equal(t, "test-app", apps.Apps[0].Slug) +} + +func TestFetchAppsListFallbackOnPrimaryFailure(t *testing.T) { + var fallbackWasCalled atomic.Bool + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte("primary is down")) + })) + t.Cleanup(primary.Close) + + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/apps.json", r.URL.Path) + w.WriteHeader(http.StatusOK) + _, _ = w.Write(newTestAppsJSON()) + fallbackWasCalled.Store(true) + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + apps, err := FetchAppsList(t.Context()) + require.NoError(t, err) + require.Len(t, apps.Apps, 1) + assert.Equal(t, "test-app", apps.Apps[0].Slug) + assert.True(t, fallbackWasCalled.Load()) +} + +func TestFetchAppsListFallbackOnPrimary404(t *testing.T) { + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + t.Cleanup(primary.Close) + + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(newTestAppsJSON()) + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + apps, err := FetchAppsList(t.Context()) + require.NoError(t, err) + require.Len(t, apps.Apps, 1) +} + +func TestFetchAppsListBothFail(t *testing.T) { + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte("primary down")) + })) + t.Cleanup(primary.Close) + + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + _, _ = w.Write([]byte("fallback down")) + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + _, err := FetchAppsList(t.Context()) + require.Error(t, err) + assert.Contains(t, err.Error(), "primary") + assert.Contains(t, err.Error(), "fallback") +} + +func TestFetchAppsListFallbackOnPrimaryNetworkError(t *testing.T) { + // Start and immediately close the primary to simulate a connection-refused error. + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + primary.Close() + + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(newTestAppsJSON()) + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + apps, err := FetchAppsList(t.Context()) + require.NoError(t, err) + require.Len(t, apps.Apps, 1) +} + +func TestFetchAppsListOnlyFallbackFails(t *testing.T) { + // Primary works; fallback is broken. Nothing should break. + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(newTestAppsJSON()) + })) + t.Cleanup(primary.Close) + + // Closed server as broken fallback. + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + fallback.Close() + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + apps, err := FetchAppsList(t.Context()) + require.NoError(t, err) + require.Len(t, apps.Apps, 1) +} + +func TestFetchManifestDataFallbackUsedForSlugPath(t *testing.T) { + var primaryHits, fallbackHits atomic.Int32 + + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + primaryHits.Add(1) + w.WriteHeader(http.StatusInternalServerError) + })) + t.Cleanup(primary.Close) + + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fallbackHits.Add(1) + assert.Equal(t, "/test-app.json", r.URL.Path) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"test": true}`)) + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + body, err := fetchManifestFile(t.Context(), "/test-app.json") + require.NoError(t, err) + assert.Contains(t, string(body), `"test"`) + assert.Equal(t, int32(1), primaryHits.Load(), "primary should have been tried once") + assert.Equal(t, int32(1), fallbackHits.Load(), "fallback should have been tried once") +} + +func TestFetchManifestDataPrimarySucceedsSkipsFallback(t *testing.T) { + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`{"ok": true}`)) + })) + t.Cleanup(primary.Close) + + var fallbackHits atomic.Int32 + fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fallbackHits.Add(1) + })) + t.Cleanup(fallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t) + + body, err := fetchManifestFile(t.Context(), "/something.json") + require.NoError(t, err) + assert.Contains(t, string(body), `"ok"`) + assert.Equal(t, int32(0), fallbackHits.Load(), "fallback must not be contacted when primary succeeds") +} + +func TestResolveBaseURLsDefaults(t *testing.T) { + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", "", t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", "", t) + primary, fallback := resolveBaseURLs() + assert.Equal(t, fmaOutputsBase, primary) + assert.Equal(t, fmaOutputsFallbackBase, fallback) +} + +func TestResolveBaseURLsWithOverrides(t *testing.T) { + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", "http://custom-primary", t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", "http://custom-fallback", t) + + primary, fallback := resolveBaseURLs() + assert.Equal(t, "http://custom-primary", primary) + assert.Equal(t, "http://custom-fallback", fallback) +} + +func TestDoFetchSuccess(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/test.json", r.URL.Path) + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) + })) + t.Cleanup(srv.Close) + + body, err := doFetch(t.Context(), srv.URL, "/test.json") + require.NoError(t, err) + assert.Equal(t, "ok", string(body)) +} + +func TestDoFetchNotFound(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + t.Cleanup(srv.Close) + + _, err := doFetch(t.Context(), srv.URL, "/missing.json") + require.Error(t, err) + assert.Contains(t, err.Error(), "404") +} + +func TestDoFetchServerError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadGateway) + _, _ = w.Write([]byte("bad gateway")) + })) + t.Cleanup(srv.Close) + + _, err := doFetch(t.Context(), srv.URL, "/broken.json") + require.Error(t, err) + assert.Contains(t, err.Error(), "502") +} + +func TestDoFetchNetworkError(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})) + srv.Close() // immediately close to force connection errors + + _, err := doFetch(t.Context(), srv.URL, "/anything.json") + require.Error(t, err) +} + +func TestDoFetchTruncatesLargeBodyInErrorMessage(t *testing.T) { + largeBody := make([]byte, 1024) + for i := range largeBody { + largeBody[i] = 'x' + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write(largeBody) + })) + t.Cleanup(srv.Close) + + _, err := doFetch(t.Context(), srv.URL, "/big.json") + require.Error(t, err) + // The error message should contain at most 512 bytes of body. + assert.LessOrEqual(t, len(err.Error()), 600) // 512 body + status prefix +} + +func TestFetchAppsListFallbackOverrideViaEnvVar(t *testing.T) { + primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + })) + t.Cleanup(primary.Close) + + var fallbackWasCalled atomic.Bool + customFallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write(newTestAppsJSON()) + fallbackWasCalled.Store(true) + })) + t.Cleanup(customFallback.Close) + + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", customFallback.URL, t) + + apps, err := FetchAppsList(t.Context()) + require.NoError(t, err) + require.Len(t, apps.Apps, 1) + assert.Equal(t, "test-app", apps.Apps[0].Slug) + assert.True(t, fallbackWasCalled.Load()) +} diff --git a/server/mdm/maintainedapps/testing_utils.go b/server/mdm/maintainedapps/testing_utils.go index 0036fb2738..024f62e731 100644 --- a/server/mdm/maintainedapps/testing_utils.go +++ b/server/mdm/maintainedapps/testing_utils.go @@ -3,7 +3,6 @@ package maintained_apps import ( "context" "encoding/json" - "log/slog" "net/http" "net/http/httptest" "os" @@ -44,8 +43,10 @@ func SyncApps(t *testing.T, ds fleet.Datastore) []fleet.MaintainedApp { // this call dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", srv.URL) defer dev_mode.ClearOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL") + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", srv.URL) + defer dev_mode.ClearOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL") - err := Refresh(context.Background(), ds, slog.New(slog.DiscardHandler)) + err := SyncAppsList(context.Background(), ds) require.NoError(t, err) apps, _, err := ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{ @@ -101,7 +102,7 @@ func SyncAndRemoveApps(t *testing.T, ds fleet.Datastore) { dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", srv.URL) defer dev_mode.ClearOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL") - err = Refresh(context.Background(), ds, slog.New(slog.DiscardHandler)) + err = SyncAppsList(context.Background(), ds) require.NoError(t, err) originalApps, _, err := ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{}) @@ -113,7 +114,7 @@ func SyncAndRemoveApps(t *testing.T, ds fleet.Datastore) { removedApp := appsFile.Apps[0] appsFile.Apps = appsFile.Apps[1:] - err = Refresh(context.Background(), ds, slog.New(slog.DiscardHandler)) + err = SyncAppsList(context.Background(), ds) require.NoError(t, err) modifiedApps, _, err := ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{}) @@ -128,7 +129,7 @@ func SyncAndRemoveApps(t *testing.T, ds fleet.Datastore) { // remove all apps from upstream. appsFile.Apps = []appListing{} - err = Refresh(context.Background(), ds, slog.New(slog.DiscardHandler)) + err = SyncAppsList(context.Background(), ds) require.NoError(t, err) modifiedApps, _, err = ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{}) diff --git a/server/service/testing_utils.go b/server/service/testing_utils.go index f1f2153f0d..5c2f40347e 100644 --- a/server/service/testing_utils.go +++ b/server/service/testing_utils.go @@ -15,6 +15,8 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" + "runtime" "sort" "strings" "sync" @@ -1494,12 +1496,25 @@ func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTes _, _ = w.Write(state.installerBytes) })) - // call Refresh directly (instead of SyncApps) since we're using the server above and not the file server - // created in SyncApps - err := maintained_apps.Refresh(t.Context(), ds, slog.New(slog.DiscardHandler)) - require.NoError(t, err) + // Locate the repo's apps.json so the manifest server can serve it. + _, thisFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatalf("could not locate myself to serve apps.json file") + } + repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile))) + appsJSONPath := filepath.Join(repoRoot, "ee", "maintained-apps", "outputs", "apps.json") manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/apps.json" { + b, err := os.ReadFile(appsJSONPath) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + _, _ = w.Write(b) + return + } + var state *fmaTestState state, found := states[r.URL.Path] if !found { @@ -1532,6 +1547,9 @@ func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTes installerServer.Close() }) dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t) + dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", manifestServer.URL, t) + + require.NoError(t, maintained_apps.SyncAppsList(t.Context(), ds)) } // acmeCSRSigner adapts a depot.Signer to the acme.CSRSigner interface.